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..3c82205 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.RepositoryHookRequest; 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,86 @@ 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 repository and refs which were updated */ @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 RepositoryHookRequest 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; + List mirrorSettings = getMirrorSettings(context.getSettings()); + if (mirrorSettings.isEmpty()) { + logger.debug("{}: Mirroring is not configured", repository); + } else { + logger.debug("{}: Scheduling pushes for {} remote(s) after {}", + repository, mirrorSettings.size(), request.getTrigger()); + schedulePushes(repository, mirrorSettings); } - - 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(); - } /** - * 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 +126,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); - } + schedulePushes(repository, mirrorSettings); } - } 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 +164,11 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep return results; } - protected boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) { + private void schedulePushes(Repository repository, List list) { + list.forEach(settings -> pushExecutor.schedule(new MirrorRequest(repository, settings), 5L, TimeUnit.SECONDS)); + } + private boolean validate(MirrorSettings ms, SettingsValidationErrors errors) { boolean result = true; boolean isHttp = false; @@ -331,11 +220,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 +234,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..79ec8ca --- /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 MirrorBucketProcessor}. + */ +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..cc2cf43 100644 --- a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java @@ -1,188 +1,128 @@ 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.project.Project; 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.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ScheduledExecutorService; +import java.util.Set; 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.*; /** - * Unit tests for {@link MirrorRepositoryHook} + * Unit tests for {@link MirrorRepositoryHook}. */ 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 testPostUpdate() { when(passwordEncryptor.decrypt(anyString())).thenReturn(password); Repository repo = mock(Repository.class); + when(repo.getId()).thenReturn(1); + when(repo.getScmId()).thenReturn(GitScm.ID); + + hook.postUpdate(buildContext(), new RepositoryPushHookRequest.Builder(repo).build()); - hook.postReceive(buildContext(repo), new ArrayList<>()); - verifyExecutor(); + verify(repo).getId(); + verify(repo).getScmId(); + verify(bucketedExecutor).schedule(requestCaptor.capture(), eq(5L), same(TimeUnit.SECONDS)); + + MirrorRequest request = requestCaptor.getValue(); + assertEquals(1, request.getRepositoryId()); } @Test - public void testEmptyRepositoriesNotMirrored() { + public void testPostUpdateForHgRepository() { Repository repo = mock(Repository.class); - when(repositoryService.isEmpty(repo)).thenReturn(true); + when(repo.getScmId()).thenReturn("hg"); - hook.postReceive(buildContext(repo), new ArrayList<>()); + hook.postUpdate(buildContext(), new RepositoryPushHookRequest.Builder(repo).build()); - verify(executor, never()).submit(ArgumentMatchers.any()); + verifyZeroInteractions(bucketedExecutor); } @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(); + public void testPostUpdateUnconfigured() { + Repository repo = mock(Repository.class); + when(repo.getScmId()).thenReturn(GitScm.ID); - } + Settings settings = mock(Settings.class); + when(settings.asMap()).thenReturn(Collections.emptyMap()); - @Test - public void testGetAuthenticatedUrl() throws Exception { + PostRepositoryHookContext context = mock(PostRepositoryHookContext.class); + when(context.getSettings()).thenReturn(settings); - String result = hook.getAuthenticatedUrl(mirrorRepoUrlHttp, username, password); - assertEquals(repository, result); + hook.postUpdate(context, new RepositoryPushHookRequest.Builder(repo).build()); + verifyZeroInteractions(bucketedExecutor); } @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 +155,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 +172,75 @@ 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()); + } + @Test + public void testValidateForGlobal() { + SettingsValidationErrors errors = mock(SettingsValidationErrors.class); + Settings settings = mock(Settings.class); + + hook.validate(settings, errors, Scopes.global()); + + verifyZeroInteractions(bucketedExecutor, errors, settings); } - private RepositoryHookContext buildContext(Repository repo) { - RepositoryHookContext context = mock(RepositoryHookContext.class); + @Test + public void testValidateForProject() { + SettingsValidationErrors errors = mock(SettingsValidationErrors.class); + Project project = mock(Project.class); + Settings settings = mock(Settings.class); + + hook.validate(settings, errors, Scopes.project(project)); + + verifyZeroInteractions(bucketedExecutor, errors, settings); + } + + 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 +252,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; } }