Browse Source

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
pull/63/head
Bryan Turner 6 years ago
parent
commit
2a06876c5a
  1. 22
      pom.xml
  2. 11
      src/main/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptor.java
  3. 147
      src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java
  4. 278
      src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java
  5. 33
      src/main/java/com/englishtown/bitbucket/hook/MirrorRequest.java
  6. 15
      src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java
  7. 9
      src/main/java/com/englishtown/bitbucket/hook/PasswordEncryptor.java
  8. 19
      src/main/resources/atlassian-plugin.xml
  9. 95
      src/test/java/com/englishtown/bitbucket/hook/DefaultPasswordEncryptorTest.java
  10. 155
      src/test/java/com/englishtown/bitbucket/hook/MirrorBucketProcessorTest.java
  11. 200
      src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java

22
pom.xml

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
@ -31,9 +30,8 @@
<maven.compiler.target>1.8</maven.compiler.target>
<bitbucket.version>5.5.0</bitbucket.version>
<bitbucket.data.version>${bitbucket.version}</bitbucket.data.version>
<atlassian-sal-api.version>3.1.0</atlassian-sal-api.version>
<amps.version>6.3.7</amps.version>
<bitbucket.test.version>${bitbucket.version}</bitbucket.test.version>
<amps.version>6.3.17</amps.version>
</properties>
<dependencyManagement>
@ -64,13 +62,23 @@
<artifactId>bitbucket-git-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-git-common</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.sal</groupId>
<artifactId>sal-api</artifactId>
<version>${atlassian-sal-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.atlassian.bitbucket.server</groupId>
<artifactId>bitbucket-test-util</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -96,8 +104,8 @@
<product>
<id>bitbucket</id>
<instanceId>bitbucket</instanceId>
<version>${bitbucket.version}</version>
<dataVersion>${bitbucket.data.version}</dataVersion>
<version>${bitbucket.test.version}</version>
<dataVersion>${bitbucket.test.version}</dataVersion>
</product>
</products>
</configuration>

11
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) {

147
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<MirrorRequest> {
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<MirrorRequest> 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<String> 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);
}
}
}

278
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<RepositoryPushHookRequest>, 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<MirrorRequest> 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
* <p>
* Callback method that is called just after a push is completed (or a pull request accepted).
* This hook executes <i>after</i> the processing of a push and will not block the user client.
* <p>
* 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<RefChange> refChanges) {
logger.debug("MirrorRepositoryHook: postReceive started.");
List<MirrorSettings> 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<Repository>() {
@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> 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<MirrorSettings> getMirrorSettings(Settings settings) {
private List<MirrorSettings> getMirrorSettings(Settings settings) {
return getMirrorSettings(settings, true, true, true);
}
protected List<MirrorSettings> getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) {
List<MirrorSettings> results = new ArrayList<>();
private List<MirrorSettings> getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) {
Map<String, Object> allSettings = settings.asMap();
int count = 0;
List<MirrorSettings> 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> mirrorSettings, Settings settings) {
Map<String, Object> values = new HashMap<String, Object>();
// Store each mirror setting
private void updateSettings(List<MirrorSettings> mirrorSettings, Settings settings) {
Map<String, Object> 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));
}
}

33
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;
}
}

15
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;
}

9
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
*

19
src/main/resources/atlassian-plugin.xml

@ -1,33 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<atlassian-plugin key="${project.groupId}.${project.artifactId}" name="${project.name}" plugins-version="2">
<plugin-info>
<description>${project.description}</description>
<version>${project.version}</version>
<vendor name="${project.organization.name}" url="${project.organization.url}"/>
<param name="atlassian-data-center-compatible">true</param>
<permissions>
<permission>execute_java</permission>
</permissions>
</plugin-info>
<!-- Components that are injected -->
<component-import key="applicationPropertiesService" interface="com.atlassian.bitbucket.server.ApplicationPropertiesService"/>
<component-import key="concurrencyService" interface="com.atlassian.bitbucket.concurrent.ConcurrencyService"/>
<component-import key="i18nService" interface="com.atlassian.bitbucket.i18n.I18nService"/>
<component-import key="applicationProperties" interface="com.atlassian.sal.api.ApplicationProperties"/>
<component-import key="pluginSettingsFactory" interface="com.atlassian.sal.api.pluginsettings.PluginSettingsFactory"/>
<component-import key="repositoryService" interface="com.atlassian.bitbucket.repository.RepositoryService"/>
<component-import key="scmService" interface="com.atlassian.bitbucket.scm.ScmService"/>
<component key="passwordEncryptor" class="com.englishtown.bitbucket.hook.DefaultPasswordEncryptor">
<interface>com.englishtown.bitbucket.hook.PasswordEncryptor</interface>
</component>
<component key="settingsReflectionHelper" class="com.englishtown.bitbucket.hook.DefaultSettingsReflectionHelper">
<interface>com.englishtown.bitbucket.hook.SettingsReflectionHelper</interface>
</component>
<component key="mirrorRepositoryHook" class="com.englishtown.bitbucket.hook.MirrorRepositoryHook"/>
<component key="mirrorBucketProcessor" class="com.englishtown.bitbucket.hook.MirrorBucketProcessor"/>
<component key="passwordEncryptor" class="com.englishtown.bitbucket.hook.DefaultPasswordEncryptor"/>
<component key="settingsReflectionHelper" class="com.englishtown.bitbucket.hook.DefaultSettingsReflectionHelper"/>
<!-- add our i18n resource -->
<resource type="i18n" name="i18n" location="i18n/stash-hook-mirror"/>
<repository-hook name="Mirror Repository Hook" i18n-name-key="mirror-repository-hook.name"
key="mirror-repository-hook" class="com.englishtown.bitbucket.hook.MirrorRepositoryHook">
key="mirror-repository-hook" class="bean:mirrorRepositoryHook"
configurable="true">
<description key="mirror-repository-hook.description">Mirror Hook</description>
<icon>icons/mirror-icon.png</icon>
<config-form name="Mirror Hook Config" key="mirror-repository-hook-config">

95
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);
}
}

155
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<MirrorRequest> REQUESTS = Collections.singletonList(REQUEST);
@Rule
public MockitoRule mockitoRule = MockitoJUnit.rule().silent();
@Mock
private GitScmCommandBuilder builder;
@Mock
private GitCommand<String> 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.<String>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);
}
}

200
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<MirrorRequest> bucketedExecutor;
@Mock
private GitCommand<String> 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<Runnable> argumentCaptor;
private ArgumentCaptor<MirrorRequest> 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.<String>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.<Runnable>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<String, Object> map = new HashMap<String, Object>();
Map<String, Object> 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<String, Object> map = new HashMap<String, Object>();
Map<String, Object> 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;
}
}

Loading…
Cancel
Save