Browse Source

Merge pull request #63 from bturner/bturner-use-bucketed-executor

Replace ExecutorService with BucketedExecutor.
develop
Adrian 5 years ago committed by GitHub
parent
commit
4511ad4422
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  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. 282
      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. 229
      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);
}
}
}

282
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<RepositoryHookRequest>, 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<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 repository and refs which were updated
*/
@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 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> 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<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 +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<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 +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<MirrorSettings> 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> 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 +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));
}
}

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

229
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<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 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.<Runnable>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<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 +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<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 +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;
}
}

Loading…
Cancel
Save