diff --git a/.travis.yml b/.travis.yml index c4f11b7..5d67671 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,3 @@ language: java jdk: - - oraclejdk8 \ No newline at end of file + - openjdk8 \ No newline at end of file diff --git a/README.md b/README.md index da82a7b..a5beff1 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,10 @@ https://bitbucket.org/atlassian/aui/src/master/src/soy/form.soy The plugin can be found on the Atlassian Marketplace here: https://marketplace.atlassian.com/plugins/com.englishtown.stash-hook-mirror + +Plugin properties in bitbucket.properties in file, located in the shared folder of server home directory: + +`plugin.com.englishtown.stash-hook-mirror.push.attempts=5 +plugin.com.englishtown.stash-hook-mirror.push.threads=3 +plugin.com.englishtown.stash-hook-mirror.push.timeout=120` + diff --git a/pom.xml b/pom.xml index 435e2fe..de734e9 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 com.englishtown @@ -67,13 +68,53 @@ bitbucket-git-common provided - com.atlassian.sal sal-api provided - + + com.atlassian.bitbucket.server + bitbucket-web-common + provided + + + com.atlassian.soy + soy-template-renderer-api + 4.1.3 + provided + + + com.atlassian.utils + atlassian-processutils + 1.8.3 + provided + + + com.atlassian.plugins.rest + com.atlassian.jersey-library + 6.0.2 + provided + pom + + + org.codehaus.jackson + jackson-mapper-asl + 1.9.13-atlassian-2 + provided + + + javax.servlet + javax.servlet-api + 3.0.1 + provided + + + com.atlassian.soy + atlassian-soy-spring-mvc-support + 5.0.0 + provided + com.atlassian.bitbucket.server bitbucket-test-util @@ -89,7 +130,12 @@ mockito-core test - + + com.github.tomakehurst + wiremock-standalone + 2.25.1 + test + diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java index a48220a..8750bd5 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorBucketProcessor.java @@ -5,19 +5,25 @@ 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.*; import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler; import com.atlassian.bitbucket.server.ApplicationPropertiesService; import com.atlassian.bitbucket.user.SecurityService; +import com.atlassian.utils.process.ProcessException; +import com.atlassian.utils.process.StringOutputHandler; +import com.atlassian.utils.process.Watchdog; import com.google.common.base.Strings; +import org.apache.commons.io.IOUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.URI; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.List; import java.util.Locale; @@ -49,6 +55,7 @@ public MirrorBucketProcessor(I18nService i18nService, PasswordEncryptor password this.securityService = securityService; timeout = Duration.ofSeconds(propertiesService.getPluginProperty(PROP_TIMEOUT, 120L)); + log.debug(PROP_TIMEOUT+": "+timeout.getSeconds()); } @Override @@ -75,16 +82,21 @@ public void process(@Nonnull String key, @Nonnull List requests) return null; } runMirrorCommand(request.getSettings(), repository); - return null; }); } - private void runMirrorCommand(MirrorSettings settings, Repository repository) { + public void runMirrorCommand(MirrorSettings settings, Repository repository) { + runMirrorCommand(settings, repository, null); + } + public void runMirrorCommand(MirrorSettings settings, Repository repository, StringOutputHandler outputHandler) { log.debug("{}: Preparing to push changes to mirror", repository); - String password = passwordEncryptor.decrypt(settings.password); - String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password); + String plainPassword = passwordEncryptor.decrypt(settings.password); + String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken); + PasswordHandler passwordHandler = new PasswordHandler(plainPassword,plainPrivateToken, + new GitCommandExitHandler(i18nService, repository)); + String authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, plainPassword); // 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 @@ -116,16 +128,36 @@ private void runMirrorCommand(MirrorSettings settings, Repository repository) { builder.argument("+refs/notes/*:refs/notes/*"); } - PasswordHandler passwordHandler = new PasswordHandler(settings.password, - new GitCommandExitHandler(i18nService, repository)); Command command = builder.errorHandler(passwordHandler) .exitHandler(passwordHandler) .build(passwordHandler); command.setTimeout(timeout); - - Object result = command.call(); - log.info("{}: Push completed with the following output:\n{}", repository, result); + Object result=null; + try { + result = command.call(); + if(result != null) { + passwordHandler.cleanText(result.toString()); + } + } catch (Exception e) { + if (outputHandler != null) { + try { + outputHandler.process(new ByteArrayInputStream(passwordHandler.getOutput().getBytes())); + } catch (ProcessException s) { + log.error("Failed to process response: " + s.getMessage()); + } + } + log.info("{}: Push failed with the following output:\n{}", repository, passwordHandler.getOutput()); + throw e; + } + if (outputHandler != null) { + try { + outputHandler.process(new ByteArrayInputStream(passwordHandler.getOutput().getBytes())); + } catch (ProcessException s) { + log.error("Failed to process response: " + s.getMessage()); + } + } + log.debug("{}: Push completed with the following output:\n{}", repository, passwordHandler.getOutput()); } String getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) { diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRemoteAdmin.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRemoteAdmin.java new file mode 100644 index 0000000..579b43f --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRemoteAdmin.java @@ -0,0 +1,120 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler; +import com.atlassian.utils.process.ProcessException; +import com.atlassian.utils.process.StringOutputHandler; +import com.sun.jersey.api.client.Client; +import com.sun.jersey.api.client.ClientResponse; +import com.sun.jersey.api.client.WebResource; +import com.sun.jersey.api.client.config.ClientConfig; +import com.sun.jersey.api.client.config.DefaultClientConfig; +import org.codehaus.jackson.JsonNode; +import org.codehaus.jackson.map.ObjectMapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.MediaType; +import java.io.ByteArrayInputStream; +import java.io.IOException; + +public class MirrorRemoteAdmin { + private static final Logger log = LoggerFactory.getLogger(MirrorRemoteAdmin.class); + private final PasswordEncryptor passwordEncryptor; + private final I18nService i18nService; + + Client client; + + public MirrorRemoteAdmin(PasswordEncryptor passwordEncryptor, I18nService i18nService) { + this.passwordEncryptor = passwordEncryptor; + this.i18nService = i18nService; + ClientConfig config = new DefaultClientConfig(); + this.client = Client.create(config); + } + + private void addToStream(StringOutputHandler outputHandler, String text) { + try { + outputHandler.process(new ByteArrayInputStream(text.getBytes())); + } catch (ProcessException e) { + log.error("Failed to process response: " + e.getMessage()); + } + } + + public void delete(MirrorSettings settings, Repository repository, StringOutputHandler outputHandler) { + String plainPassword = passwordEncryptor.decrypt(settings.password); + String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken); + PasswordHandler passwordHandler = new PasswordHandler(plainPassword, plainPrivateToken, + new GitCommandExitHandler(i18nService, repository)); + RuntimeException e = null; + try { + delete(settings, repository, passwordHandler); + } catch (RuntimeException deleteException) { + e = deleteException; + } + addToStream(outputHandler, passwordHandler.getOutput()); + if (e != null) { + throw e; + } + } + + private void delete(MirrorSettings settings, Repository repository, PasswordHandler passwordHandler) { + ObjectMapper mapper = new ObjectMapper(); + String plainPrivateToken = passwordEncryptor.decrypt(settings.privateToken); + + if (settings.restApiURL.isEmpty()) { + log.error("Remote REST Api URL not configured for " + repository.getName()); + return; + } + WebResource webResource = client + .resource(settings.restApiURL + "/api/v4/projects") + .queryParam("search", repository.getName()); + + WebResource.Builder webResourceBuilder= webResource.getRequestBuilder().accept(MediaType.APPLICATION_JSON); + if (!settings.privateToken.isEmpty()) { + webResourceBuilder = webResourceBuilder.header("PRIVATE-TOKEN",plainPrivateToken); + } + + ClientResponse response = webResourceBuilder.get(ClientResponse.class); + if (response.getStatus() != 200) { + addToStream(passwordHandler, response.toString()); + throw new RuntimeException("Failed : HTTP error code : " + + response.getStatus()); + } + JsonNode output; + try { + output = mapper.readTree(response.getEntityInputStream()); + } catch (IOException e) { + addToStream(passwordHandler, e.toString()); + throw new RuntimeException("Failed : Invalid response data from " + settings.restApiURL + " : " + e.getMessage()); + } + Integer repoId = null; + for (JsonNode project : output) { + if (!project.has("name_with_namespace")) { + continue; + } + if (project.get("name_with_namespace").asText().equals(repository.getProject().getName() + " / " + repository.getName())) { + repoId = project.get("id").asInt(); + break; + } + } + if (repoId == null) { + addToStream(passwordHandler, response.toString()); + throw new RuntimeException("Remote repository not found"); + } + + webResource = client + .resource(settings.restApiURL + "/api/v4/projects/" + repoId); + webResourceBuilder= webResource.getRequestBuilder().accept(MediaType.APPLICATION_JSON); + if (!settings.privateToken.isEmpty()) { + webResourceBuilder = webResourceBuilder.header("PRIVATE-TOKEN",plainPrivateToken); + } + response = webResourceBuilder.delete(ClientResponse.class); + if (response.getStatus() != 202) { + addToStream(passwordHandler, response.toString()); + throw new RuntimeException("Failed : HTTP error code : " + + response.getStatus()); + } + addToStream(passwordHandler, response.toString()); + } +} diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationCondition.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationCondition.java new file mode 100644 index 0000000..052c9c2 --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationCondition.java @@ -0,0 +1,35 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.hook.repository.RepositoryHook; +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scope.Scopes; +import com.atlassian.plugin.PluginParseException; +import com.atlassian.plugin.web.Condition; + +import java.util.Map; + +public class MirrorRepositoryHasConfigurationCondition implements Condition { + + public static final String REPOSITORY = "repository"; + private final RepositoryHookService repositoryHookService; + + public MirrorRepositoryHasConfigurationCondition(RepositoryHookService repositoryHookService) { + this.repositoryHookService = repositoryHookService; + } + + @Override + public void init(Map map) throws PluginParseException { + } + + @Override + public boolean shouldDisplay(Map context) { + Repository repository = (Repository) context.get(REPOSITORY); + RepositoryHook repositoryHook = repositoryHookService.getByKey(Scopes.repository(repository), "com.englishtown.stash-hook-mirror:mirror-repository-hook"); + if (repositoryHook == null) { + /* Hook not installed. How do I get here :)? */ + return false; + } + return repositoryHook.isConfigured() && repositoryHook.isEnabled(); + } +} diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java index 643904d..a58ae67 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryHook.java @@ -4,9 +4,14 @@ import com.atlassian.bitbucket.concurrent.BucketedExecutorSettings; import com.atlassian.bitbucket.concurrent.ConcurrencyPolicy; import com.atlassian.bitbucket.concurrent.ConcurrencyService; +import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent; +import com.atlassian.bitbucket.event.repository.RepositoryModifiedEvent; import com.atlassian.bitbucket.hook.repository.*; +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.project.Project; import com.atlassian.bitbucket.repository.Repository; import com.atlassian.bitbucket.scm.git.GitScm; +import com.atlassian.bitbucket.scope.ProjectScope; import com.atlassian.bitbucket.scope.RepositoryScope; import com.atlassian.bitbucket.scope.Scope; import com.atlassian.bitbucket.scope.ScopeVisitor; @@ -14,14 +19,19 @@ import com.atlassian.bitbucket.setting.Settings; import com.atlassian.bitbucket.setting.SettingsValidationErrors; import com.atlassian.bitbucket.setting.SettingsValidator; +import com.atlassian.event.api.EventListener; +import com.atlassian.utils.process.StringOutputHandler; import com.google.common.collect.ImmutableSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; +import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class MirrorRepositoryHook implements PostRepositoryHook, SettingsValidator { @@ -35,6 +45,11 @@ public class MirrorRepositoryHook implements PostRepositoryHook pushExecutor; + private BucketedExecutor pushExecutor; private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); public MirrorRepositoryHook(ConcurrencyService concurrencyService, + I18nService i18nService, PasswordEncryptor passwordEncryptor, ApplicationPropertiesService propertiesService, MirrorBucketProcessor pushProcessor, + MirrorRemoteAdmin mirrorRemoteAdmin, + RepositoryHookService repositoryHookService, SettingsReflectionHelper settingsReflectionHelper) { logger.debug("MirrorRepositoryHook: init started"); + this.concurrencyService = concurrencyService; + this.i18nService = i18nService; this.passwordEncryptor = passwordEncryptor; + this.propertiesService = propertiesService; + this.pushProcessor = pushProcessor; + this.mirrorRemoteAdmin = mirrorRemoteAdmin; + this.repositoryHookService = repositoryHookService; this.settingsReflectionHelper = settingsReflectionHelper; + pushExecutor = createPushExecutor(); + logger.debug("MirrorRepositoryHook: init completed"); + } + + private BucketedExecutor createPushExecutor() { + logger.debug("MirrorRepositoryHook: initialize pushExecutor"); int attempts = propertiesService.getPluginProperty(PROP_ATTEMPTS, 5); + logger.debug(PROP_ATTEMPTS + ": " + attempts); int threads = propertiesService.getPluginProperty(PROP_THREADS, 3); - - pushExecutor = concurrencyService.getBucketedExecutor(getClass().getSimpleName(), + logger.debug(PROP_THREADS + ": " + threads); + return 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"); } /** @@ -81,6 +117,7 @@ public MirrorRepositoryHook(ConcurrencyService concurrencyService, */ @Override public void postUpdate(@Nonnull PostRepositoryHookContext context, @Nonnull RepositoryHookRequest request) { + logger.debug("postUpdate " + request.getRepository().getName()); if (TRIGGERS_TO_IGNORE.contains(request.getTrigger())) { logger.trace("MirrorRepositoryHook: skipping trigger {}", request.getTrigger()); return; @@ -117,7 +154,14 @@ public Repository visit(@Nonnull RepositoryScope scope) { return scope.getRepository(); } }); - if (repository == null) { + Project project = scope.accept(new ScopeVisitor() { + + @Override + public Project visit(@Nonnull ProjectScope scope) { + return scope.getProject(); + } + }); + if (repository == null && project == null) { return; } @@ -134,8 +178,11 @@ public Repository visit(@Nonnull RepositoryScope scope) { // If no errors, run the mirror command if (ok) { + logger.error("update settings"); updateSettings(mirrorSettings, settings); - schedulePushes(repository, mirrorSettings); + if(repository != null) { + schedulePushes(repository, mirrorSettings); + } } } catch (Exception e) { logger.error("Error running MirrorRepositoryHook validate.", e); @@ -143,11 +190,77 @@ public Repository visit(@Nonnull RepositoryScope scope) { } } - private List getMirrorSettings(Settings settings) { + @EventListener + public void repositoryDeleted(RepositoryDeletedEvent repositoryDeletedEvent) { + logger.debug("Delete " + repositoryDeletedEvent.getRepository().getName()); + deleteRepository(repositoryDeletedEvent.getRepository()); + } + + @EventListener + public void repositoryModified(RepositoryModifiedEvent repositoryModifiedEvent) { + logger.debug("Modify " + repositoryModifiedEvent.getRepository().getName()); + if (repositoryModifiedEvent.getOldValue().getName().equals(repositoryModifiedEvent.getNewValue().getName()) + && repositoryModifiedEvent.getOldValue().getProject().getKey().equals(repositoryModifiedEvent.getNewValue().getProject().getKey())) { + logger.debug("Repository project and name not changed. Nothing to mirror"); + return; + } + + List newMirrorSettingsList = getMirrorSettings(getPluginSettings(repositoryModifiedEvent.getNewValue())); + for (MirrorSettings mirrorSettings : newMirrorSettingsList) { + interpolateMirrorRepoUrl(repositoryModifiedEvent.getNewValue(), mirrorSettings); + } + + deleteRepository(repositoryModifiedEvent.getOldValue()); + createRepositoryMirrors(repositoryModifiedEvent.getNewValue(),newMirrorSettingsList); + } + + private void deleteRepository(Repository repository) { + Settings settings = getPluginSettings(repository); + List oldMirrorSettingsList = getMirrorSettings(settings); + for (MirrorSettings mirrorSettings : oldMirrorSettingsList) { + if (!mirrorSettings.restApiURL.isEmpty()) { + // Delete old repository + StringOutputHandler outputHandler=new StringOutputHandler(); + try { + logger.debug("Delete mirror for " + mirrorSettings.restApiURL); + mirrorRemoteAdmin.delete(mirrorSettings, repository, outputHandler); + } catch (Exception e) { + logger.debug("Deleting Mirroring failed with " + outputHandler.getOutput() + e ); + } + } + } + } + + private void createRepositoryMirrors(Repository repository,List newMirrorSettingsList) { + for (MirrorSettings mirrorSettings : newMirrorSettingsList) { + // Create new repository + StringOutputHandler outputHandler=new StringOutputHandler(); + try { + logger.debug("Trigger mirror for " + mirrorSettings.mirrorRepoUrl); + pushProcessor.runMirrorCommand(mirrorSettings, repository, outputHandler); + } catch (Exception e) { + logger.debug("Mirroring failed with " + outputHandler.getOutput() + e); + } + } + } + + public Settings getPluginSettings(Repository repository) { + RepositoryScope repositoryScope = new RepositoryScope(repository); + RepositoryHookSettings repositoryHookSettings = repositoryHookService.getSettings(new GetRepositoryHookSettingsRequest.Builder(repositoryScope, "com.englishtown.stash-hook-mirror:mirror-repository-hook").build()); + if (repositoryHookSettings != null) { + return repositoryHookSettings.getSettings(); + } + return repositoryHookService.createSettingsBuilder().build(); + } + + public List getMirrorSettings(Settings settings) { return getMirrorSettings(settings, true, true, true); } - private List getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) { + public List getMirrorSettings(Settings settings, boolean defTags, boolean defNotes, boolean defAtomic) { + if(settings == null){ + return null; + } Map allSettings = settings.asMap(); int count = 0; @@ -164,17 +277,71 @@ private List getMirrorSettings(Settings settings, boolean defTag ms.tags = (settings.getBoolean(SETTING_TAGS + suffix, defTags)); ms.notes = (settings.getBoolean(SETTING_NOTES + suffix, defNotes)); ms.atomic = (settings.getBoolean(SETTING_ATOMIC + suffix, defAtomic)); + ms.restApiURL = (settings.getString(SETTING_REST_API_URL + suffix, "")); + ms.privateToken = (settings.getString(SETTING_PRIVATE_TOKEN + suffix, "")); ms.suffix = String.valueOf(count++); results.add(ms); } } - return results; } - private void schedulePushes(Repository repository, List list) { - list.forEach(settings -> pushExecutor.schedule(new MirrorRequest(repository, settings), 5L, TimeUnit.SECONDS)); + private MirrorSettings interpolateMirrorRepoUrl(Repository repository, MirrorSettings settings) { + settings.mirrorRepoUrl = interpolateMirrorRepoUrl(repository, settings.mirrorRepoUrl); + return settings; + } + + public String interpolateMirrorRepoUrl(Repository repository, String mirrorRepoUrl) { + Matcher p = PLACEHOLDER_REGEX.matcher(mirrorRepoUrl); + StringBuffer stringBuffer = new StringBuffer(); + while (p.find()) { + Matcher o = OBJECT_REGEX.matcher(p.group(1)); + Matcher m = METHOD_REGEX.matcher(p.group(1)); + try { + if (!o.find()) { + throw new NoSuchFieldException("Object not referenced"); + } + Object instance = null; + /* Cannot get method argument from reflection. Assign well known names */ + switch (o.group(1)) { + case "repository": + instance = repository; + break; + default: + throw new NoSuchFieldException("Unknown object " + o.group(1)); + } + while (m.find()) { + instance = instance.getClass().getMethod(m.group(1)).invoke(instance); + } + if(instance.equals(repository)) { + throw new NoSuchMethodException("Not repository method specified"); + } + p.appendReplacement(stringBuffer, Matcher.quoteReplacement(instance.toString())); + } catch (NoSuchFieldException | NoSuchMethodException e) { + logger.error("Failed to interpolate expression " + p.group(0) + " " + e); + throw new RuntimeException("Failed to interpolate expression " + p.group(0) + " " + e); + //p.appendReplacement(stringBuffer, Matcher.quoteReplacement(p.group(0))); + } catch (IllegalAccessException | InvocationTargetException e) { + p.appendReplacement(stringBuffer, Matcher.quoteReplacement(p.group(0))); + } + } + p.appendTail(stringBuffer); + return stringBuffer.toString(); + } + + public void schedulePushes(Repository repository, List list) { + for (MirrorSettings settings : list) { + MirrorRequest request = new MirrorRequest(repository, interpolateMirrorRepoUrl(repository, settings)); + try { + pushExecutor.schedule(request, 5L, TimeUnit.SECONDS); + } catch (ClassCastException e) { + /* Quick and dirty way to re-initialize pushExecutor after plugin update */ + pushExecutor.shutdown(); + pushExecutor = createPushExecutor(); + pushExecutor.schedule(request, 5L, TimeUnit.SECONDS); + } + } } private boolean validate(MirrorSettings ms, SettingsValidationErrors errors) { @@ -239,6 +406,8 @@ private void updateSettings(List mirrorSettings, Settings settin values.put(SETTING_TAGS + ms.suffix, ms.tags); values.put(SETTING_NOTES + ms.suffix, ms.notes); values.put(SETTING_ATOMIC + ms.suffix, ms.atomic); + values.put(SETTING_REST_API_URL + ms.suffix, ms.restApiURL); + values.put(SETTING_PRIVATE_TOKEN + ms.suffix, (ms.privateToken.isEmpty() ? ms.privateToken : passwordEncryptor.encrypt(ms.privateToken))); } // Unfortunately the settings are stored in an immutable map, so need to cheat with reflection diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryServlet.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryServlet.java new file mode 100644 index 0000000..69aa283 --- /dev/null +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorRepositoryServlet.java @@ -0,0 +1,142 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.scm.git.command.GitCommandExitHandler; +import com.atlassian.bitbucket.setting.Settings; +import com.atlassian.soy.renderer.SoyException; +import com.atlassian.soy.renderer.SoyTemplateRenderer; +import com.atlassian.utils.process.StringOutputHandler; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +public class MirrorRepositoryServlet extends HttpServlet { + private static final Logger log = LoggerFactory.getLogger(MirrorRepositoryServlet.class); + + private final I18nService i18nService; + private final MirrorBucketProcessor pushProcessor; + private final MirrorRemoteAdmin mirrorRemoteAdmin; + private final MirrorRepositoryHook mirrorRepositoryHook; + private final RepositoryService repositoryService; + private final SoyTemplateRenderer soyTemplateRenderer; + + @SuppressWarnings("WeakerAccess") + public MirrorRepositoryServlet(I18nService i18nService, + MirrorBucketProcessor pushProcessor, + MirrorRemoteAdmin mirrorRemoteAdmin, + MirrorRepositoryHook mirrorRepositoryHook, + RepositoryService repositoryService, + SoyTemplateRenderer soyTemplateRenderer + ) { + this.i18nService = i18nService; + this.pushProcessor = pushProcessor; + this.mirrorRemoteAdmin = mirrorRemoteAdmin; + this.mirrorRepositoryHook = mirrorRepositoryHook; + this.repositoryService = repositoryService; + this.soyTemplateRenderer = soyTemplateRenderer; + } + + public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + doContinue(req,resp); + } + + public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + doContinue(req,resp); + } + + private void doContinue(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException { + Repository repository = getRepositoryFromRequest(req); + if (repository == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + Settings settings = mirrorRepositoryHook.getPluginSettings(repository); + + Map allSettings = Maps.newHashMap(settings.asMap()); + Map> configErrors = new HashMap<>(); + for (String key : allSettings.keySet()) { + if (key.startsWith(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL)) { + try { + allSettings.replace(key, mirrorRepositoryHook.interpolateMirrorRepoUrl(repository, (String) allSettings.get(key))); + } catch (Exception e) { + configErrors.put(key, new ArrayList<>(Collections.singletonList(e.getMessage()))); + } + } + } + + if(req.getMethod().equals("POST")) { + Map map = req.getParameterMap(); + map.forEach((k, v) -> Arrays.stream(v).forEach((s) -> log.debug(k + " : " + s))); + + List mirrorSettingsList = mirrorRepositoryHook.getMirrorSettings(settings); + for (MirrorSettings mirrorSettings : mirrorSettingsList) { + + if (req.getParameter("delete" + mirrorSettings.suffix) != null && !mirrorSettings.restApiURL.isEmpty()) { + StringOutputHandler outputHandler=new StringOutputHandler(); + try { + log.debug("Delete mirror for " + mirrorSettings.restApiURL); + mirrorRemoteAdmin.delete(mirrorSettings, repository, outputHandler); + allSettings.put("stdout" + mirrorSettings.suffix, outputHandler.getOutput()); + } catch (Exception e) { + log.debug("Deleting Mirroring failed with " + e.getMessage()); + allSettings.put("stderr" + mirrorSettings.suffix, e.getMessage() + "\n" + outputHandler.getOutput()); + } + } + if (req.getParameter("trigger" + mirrorSettings.suffix) != null) { + StringOutputHandler outputHandler=new StringOutputHandler(); + try { + mirrorSettings.mirrorRepoUrl = mirrorRepositoryHook.interpolateMirrorRepoUrl(repository, mirrorSettings.mirrorRepoUrl); + log.debug("Trigger mirror for " + mirrorSettings.mirrorRepoUrl); + pushProcessor.runMirrorCommand(mirrorSettings, repository, outputHandler); + allSettings.put("stdout" + mirrorSettings.suffix, outputHandler.getOutput()); + } catch (Exception e) { + log.debug("Mirroring failed with " + e); + allSettings.put("stderr" + mirrorSettings.suffix, e + "\n" + outputHandler.getOutput()); + } + } + } + } + + resp.setContentType("text/html;charset=UTF-8"); + try { + soyTemplateRenderer.render(resp.getWriter(), "com.englishtown.stash-hook-mirror:mirror-hook-action-form", + "com.englishtown.bitbucket.hook.action", + ImmutableMap + .builder() + .put("config", allSettings) + .put("errors", configErrors) + .put("repository", repository) + .build() + ); + } catch ( + SoyException e) { + Throwable cause = e.getCause(); + if (cause instanceof IOException) { + throw (IOException) cause; + } + throw new ServletException(e); + } + } + + private Repository getRepositoryFromRequest(HttpServletRequest req) { + String pathInfo = req.getPathInfo(); + String[] pathParts = pathInfo.substring(1).split("/"); + if (pathParts.length != 4) { + return null; + } + String projectKey = pathParts[1]; + String repoSlug = pathParts[3]; + + return repositoryService.getBySlug(projectKey, repoSlug); + } +} \ No newline at end of file diff --git a/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java b/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java index 7b873a5..dafcb66 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java +++ b/src/main/java/com/englishtown/bitbucket/hook/MirrorSettings.java @@ -12,4 +12,6 @@ class MirrorSettings implements Serializable { boolean tags; boolean notes; boolean atomic; + String restApiURL; + String privateToken; } diff --git a/src/main/java/com/englishtown/bitbucket/hook/PasswordHandler.java b/src/main/java/com/englishtown/bitbucket/hook/PasswordHandler.java index 99ae952..1089d0f 100644 --- a/src/main/java/com/englishtown/bitbucket/hook/PasswordHandler.java +++ b/src/main/java/com/englishtown/bitbucket/hook/PasswordHandler.java @@ -3,10 +3,12 @@ import com.atlassian.bitbucket.scm.CommandErrorHandler; import com.atlassian.bitbucket.scm.CommandExitHandler; import com.atlassian.bitbucket.scm.CommandOutputHandler; +import com.atlassian.utils.process.ProcessException; import com.atlassian.utils.process.StringOutputHandler; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.ByteArrayInputStream; /** * Handles removing passwords from output text @@ -14,21 +16,28 @@ class PasswordHandler extends StringOutputHandler implements CommandOutputHandler, CommandErrorHandler, CommandExitHandler { - private final String target; + private final String passwordText; + private final String privateTokenText; private final CommandExitHandler exitHandler; private static final String PASSWORD_REPLACEMENT = ":*****@"; + private static final String TOKEN_REPLACEMENT = "*****"; - public PasswordHandler(String password, CommandExitHandler exitHandler) { + public PasswordHandler(String password, String privateToken, CommandExitHandler exitHandler) { this.exitHandler = exitHandler; - this.target = ":" + password + "@"; + this.passwordText = ":" + password + "@"; + this.privateTokenText = privateToken; } public String cleanText(String text) { if (text == null || text.isEmpty()) { return text; } - return text.replace(target, PASSWORD_REPLACEMENT); + String truncatedText=text.replace(passwordText, PASSWORD_REPLACEMENT); + if(!privateTokenText.isEmpty()) { + truncatedText=truncatedText.replace(privateTokenText,TOKEN_REPLACEMENT); + } + return truncatedText; } @Override @@ -45,6 +54,5 @@ public void onCancel(@Nonnull String command, int exitCode, @Nullable String std public void onExit(@Nonnull String command, int exitCode, @Nullable String stdErr, @Nullable Throwable thrown) { exitHandler.onExit(cleanText(command), exitCode, cleanText(stdErr), thrown); } - } diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index dee3f5b..5aeea07 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -17,9 +17,19 @@ + + + ; + ; + + + + @@ -35,5 +45,48 @@ com.englishtown.bitbucket.hook.view + + project + repository + + + + + + REPO_ADMIN + + + + aui-sidebar-settings-button + + + com.atlassian.auiplugin:ajs + com.atlassian.auiplugin:message + + + atl.general + + + /mirror-hook/* + + + + + /**/*.soy + /**/*.js + + com.atlassian.bitbucket.server.bitbucket-web:server-soy-templates + com.atlassian.bitbucket.server.bitbucket-web:global + + diff --git a/src/main/resources/css/mirror-hook.css b/src/main/resources/css/mirror-hook.css new file mode 100644 index 0000000..796fc66 --- /dev/null +++ b/src/main/resources/css/mirror-hook.css @@ -0,0 +1,4 @@ +.icon-mirror { + background-image: url('icons/mirror-icon-small.png'); +} + diff --git a/src/main/resources/i18n/stash-hook-mirror.properties b/src/main/resources/i18n/stash-hook-mirror.properties index b256001..afbc543 100644 --- a/src/main/resources/i18n/stash-hook-mirror.properties +++ b/src/main/resources/i18n/stash-hook-mirror.properties @@ -17,3 +17,12 @@ mirror-repository-hook.refspec.description=The git refspec(s) to mirror (default mirror-repository-hook.tags.label=Tags (ie. +refs/tags/*:refs/tags/*) mirror-repository-hook.notes.label=Notes (ie. +refs/notes/*:refs/notes/*) mirror-repository-hook.atomic.label=Atomic + +mirror-repository-hook.restApiURL.label=REST API URL +mirror-repository-hook.restApiURL.description=The REST API URL for repository management (Support gitlab). + +mirror-repository-hook.privateToken.label=Access token +mirror-repository-hook.privateToken.description=The REST API Personal Access Token. + +mirror-repository-hook.mirror.action=Mirror +mirror-repository-hook.mirror.action.heading=Manually trigger mirroring {0} / {1} \ No newline at end of file diff --git a/src/main/resources/icons/mirror-icon-small.png b/src/main/resources/icons/mirror-icon-small.png new file mode 100644 index 0000000..6355c6d Binary files /dev/null and b/src/main/resources/icons/mirror-icon-small.png differ diff --git a/src/main/resources/static/mirror-repository-hook-execute-form.soy b/src/main/resources/static/mirror-repository-hook-execute-form.soy new file mode 100644 index 0000000..50348cb --- /dev/null +++ b/src/main/resources/static/mirror-repository-hook-execute-form.soy @@ -0,0 +1,97 @@ +{namespace com.englishtown.bitbucket.hook} + +/** + * @param config + * @param errors + * @param repository + */ +{template .action} + + + + + + + + + {call widget.aui.pageHeader} + {param content} +

{getText('mirror-repository-hook.mirror.action.heading', $repository.project.key, $repository.slug)}

+ {/param} + {/call} + {call aui.form.form} + {param action: '' /} + {param content} + + {foreach $index in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]} + {if $config['mirrorRepoUrl' + $index]} + {call aui.form.form} + {param action: '' /} + {param content} +
+ +

{$config['mirrorRepoUrl' + $index]}

+ {if $errors['mirrorRepoUrl' + $index]} + {call widget.aui.form.fieldErrors} + {param id: 'errors' + $index /} + {param errors: $errors['mirrorRepoUrl' + $index] /} + {/call} + {/if} + {if $config['restApiURL' + $index]} + {call widget.aui.form.submit} + {param id: 'delete' + $index /} + {param label: 'Delete from '+$config['restApiURL' + $index]/} + {/call} + {/if} + + {if $config['stdout' + $index]} + {call aui.message.success} + {param isCloseable: true /} + {param content} +
+                                            
+                                                {$config['stdout' + $index]}
+                                            
+                                        
+ {/param} + {/call} + {/if} + {if $config['stderr' + $index]} + {call aui.message.error} + {param isCloseable: true /} + {param content} +
+                                            
+                                                {$config['stderr' + $index]}
+                                            
+                                        
+ {/param} + {/call} + {/if} +
+ {/param} + {/call} + {/if} + {/foreach} +/** {call aui.form.form} + {param action: '' /} + {param content} + {call widget.aui.form.submit} + {param id: 'triggerAll' /} + {param label: 'Trigger All' /} + {/call} + + {/param} + {/call} +*/ + {/param} + {/call} + + +{/template} diff --git a/src/main/resources/static/mirror-repository-hook.js b/src/main/resources/static/mirror-repository-hook.js index 67c030d..4fd7a45 100644 --- a/src/main/resources/static/mirror-repository-hook.js +++ b/src/main/resources/static/mirror-repository-hook.js @@ -27,8 +27,8 @@ define('et/hook/mirror', ['jquery', 'exports'], function ($, exports) { function addRemoveButton() { // Select all fieldset groups that don't have a remove button var group = $(".et-mirror-group").not(":has(.et-remove-button)"); - var html = createButton({text: 'Remove', extraClasses: 'et-remove-button add-hook-button', extraAttributes: 'type=button'}); - group.find('.et-mirror-repo input').after(html); + var html = "
"+createButton({text: 'Remove', extraClasses: 'et-remove-button add-hook-button', extraAttributes: 'type=button'})+"
"; + group.find('.et-mirror-repo textarea').after(html); group.find('.et-remove-button').click(function (e) { $(e.currentTarget).parents('.et-mirror-group').remove(); diff --git a/src/main/resources/static/mirror-repository-hook.soy b/src/main/resources/static/mirror-repository-hook.soy index 71026d3..8c74ba1 100644 --- a/src/main/resources/static/mirror-repository-hook.soy +++ b/src/main/resources/static/mirror-repository-hook.soy @@ -42,7 +42,7 @@ */ {template .subview}
- {call aui.form.textField} + {call aui.form.textareaField} {param id: 'mirrorRepoUrl' + $index /} {param value: $config['mirrorRepoUrl' + $index] /} {param labelContent} @@ -51,6 +51,7 @@ {param isRequired: true /} {param descriptionText: getText('mirror-repository-hook.mirrorRepoUrl.description') /} {param extraClasses: 'et-mirror-repo' /} + {param fieldWidth: 'long' /} {param errorTexts: $errors ? $errors['mirrorRepoUrl' + $index] : null /} {/call} {call aui.form.textField} @@ -101,6 +102,24 @@ ] ] /} {/call} + {call aui.form.textField} + {param id: 'restApiURL' + $index /} + {param value: $config['restApiURL' + $index] /} + {param labelContent} + {getText('mirror-repository-hook.restApiURL.label')} + {/param} + {param descriptionText: getText('mirror-repository-hook.restApiURL.description') /} + {param errorTexts: $errors ? $errors['restApiURL' + $index] : null /} + {/call} + {call aui.form.passwordField} + {param id: 'privateToken' + $index /} + {param value: $config['privateToken' + $index] /} + {param labelContent} + {getText('mirror-repository-hook.privateToken.label')} + {/param} + {param descriptionText: getText('mirror-repository-hook.privateToken.description') /} + {param errorTexts: $errors ? $errors['privateToken' + $index] : null /} + {/call}
{/template} diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorRemoteAdminTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorRemoteAdminTest.java new file mode 100644 index 0000000..f25216c --- /dev/null +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRemoteAdminTest.java @@ -0,0 +1,216 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scm.CommandExitHandler; +import com.atlassian.utils.process.ProcessException; +import com.atlassian.utils.process.StringOutputHandler; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.AdditionalAnswers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import javax.ws.rs.core.MediaType; + +import java.io.InputStream; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link MirrorRemoteAdmin}. + */ +public class MirrorRemoteAdminTest { + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule().silent(); + + private MirrorRemoteAdmin mirrorRemoteAdmin; + private CommandExitHandler exitHandler; + //private PasswordHandler handler; + private Repository repo; + private Project project; + @Mock + private PasswordEncryptor passwordEncryptor; + @Mock + private I18nService i18nService; + @Mock + private StringOutputHandler handler; + + @Rule + public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort().dynamicHttpsPort()); + + @Before + public void setup() { + mirrorRemoteAdmin=new MirrorRemoteAdmin(passwordEncryptor,i18nService); + exitHandler = mock(CommandExitHandler.class); + //handler = new PasswordHandler("password", "privateToken", exitHandler); + + repo = mock(Repository.class); + when(repo.getName()).thenReturn("test"); + project = mock(Project.class); + when(project.getKey()).thenReturn("PROJECT"); + when(project.getName()).thenReturn("PROJECT"); + when(repo.getProject()).thenReturn(project); + when(passwordEncryptor.decrypt(anyString())).then(AdditionalAnswers.returnsFirstArg()); + } + @Test + public void testDeleteNotConfigured() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorRemoteAdmin.delete(mirrorSettings, repo, handler); + } + + @Test + public void testDelete404() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlMatching(".*")) + .willReturn(aResponse() + .withStatus(404))); + + try { + mirrorRemoteAdmin.delete(mirrorSettings, repo, handler); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed : HTTP error code : 404", e.getMessage()); + } + } + @Test + public void testDeleteBrokenInputStream() throws ProcessException { + + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlMatching(".*")) + .willReturn(aResponse() + .withStatus(404))); + + PasswordHandler brokenOutput = mock(PasswordHandler.class, withSettings().verboseLogging()); + doThrow(new ProcessException("Output porcessing exception")).when(brokenOutput).process(any(InputStream.class)); + + try { + mirrorRemoteAdmin.delete(mirrorSettings, repo, brokenOutput); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed : HTTP error code : 404", e.getMessage()); + } + } + + @Test + public void testDeleteInvalidResponseData() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlEqualTo("/api/v4/projects?search=test")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody("Not JSON data"))); + try { + mirrorRemoteAdmin.delete(mirrorSettings, repo, handler); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().startsWith("Failed : Invalid response data from")); + } + } + @Test + public void testDeleteNoRepo() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlEqualTo("/api/v4/projects?search=test")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody("[{\"id\":1,\"name_with_namespace\":\"PROJECT / testing\"},{\"id\":3,\"name_with_namespace\":\"OTHER / test\"}]"))); + try { + mirrorRemoteAdmin.delete(mirrorSettings, repo, handler); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().startsWith("Remote repository not found")); + } + } + @Test + public void testDeleteNoPermissions() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlEqualTo("/api/v4/projects?search=test")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody("[{\"id\":1,\"path_with_namespace\":\"PROJECT / testing\"},{\"id\":2,\"name_with_namespace\":\"PROJECT / test\"},{\"id\":3,\"name_with_namespace\":\"OTHER/test\"}]"))); + + stubFor(delete(urlEqualTo("/api/v4/projects/2")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(403) + .withHeader("Content-Type", MediaType.TEXT_PLAIN) + .withBody("No permissions"))); + try { + mirrorRemoteAdmin.delete(mirrorSettings,repo,handler); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed : HTTP error code : 403", e.getMessage()); + } + } + + @Test + public void testDeleteSuccess() { + MirrorSettings mirrorSettings = emptySettings(); + mirrorSettings.restApiURL = "http://localhost:" + wireMockRule.port(); + mirrorSettings.privateToken = "PRIVATETOKEN"; + + stubFor(get(urlEqualTo("/api/v4/projects?search=test")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", MediaType.APPLICATION_JSON) + .withBody("[{\"id\":1,\"name_with_namespace\":\"PROJECT / testing\"},{\"id\":2,\"name_with_namespace\":\"PROJECT / test\"},{\"id\":3,\"name_with_namespace\":\"OTHER / test\"}]"))); + + stubFor(delete(urlEqualTo("/api/v4/projects/2")) + .withHeader("Accept", equalTo(MediaType.APPLICATION_JSON)) + .withHeader("PRIVATE-TOKEN", equalTo(mirrorSettings.privateToken)) + .willReturn(aResponse() + .withStatus(202))); + + mirrorRemoteAdmin.delete(mirrorSettings,repo,handler); + } + + private MirrorSettings emptySettings() { + MirrorSettings ms=new MirrorSettings(); + ms.mirrorRepoUrl=""; + ms.username=""; + ms.password=""; + ms.suffix="0"; + ms.refspec=""; + ms.tags=false; + ms.notes=false; + ms.atomic=false; + ms.restApiURL=""; + ms.privateToken=""; + return ms; + } +} \ No newline at end of file diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationConditionTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationConditionTest.java new file mode 100644 index 0000000..3c176e2 --- /dev/null +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHasConfigurationConditionTest.java @@ -0,0 +1,39 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.hook.repository.RepositoryHook; +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.scope.RepositoryScope; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class MirrorRepositoryHasConfigurationConditionTest { + @Test + public void testConditionConfigured() { + RepositoryHookService repositoryHookService = mock(RepositoryHookService.class); + RepositoryHook repositoryHook = mock(RepositoryHook.class); + MirrorRepositoryHasConfigurationCondition condition = new MirrorRepositoryHasConfigurationCondition(repositoryHookService); + condition.init(Collections.EMPTY_MAP); + Repository repo = mock(Repository.class); + Map context = Collections.singletonMap("repository", repo); + + Assert.assertFalse(condition.shouldDisplay(context)); + + when(repositoryHookService.getByKey(any(RepositoryScope.class), eq("com.englishtown.stash-hook-mirror:mirror-repository-hook"))).thenReturn(repositoryHook); + Assert.assertFalse(condition.shouldDisplay(context)); + + when(repositoryHook.isConfigured()).thenReturn(true); + when(repositoryHook.isEnabled()).thenReturn(false); + Assert.assertFalse(condition.shouldDisplay(context)); + + when(repositoryHook.isConfigured()).thenReturn(true); + when(repositoryHook.isEnabled()).thenReturn(true); + Assert.assertTrue(condition.shouldDisplay(context)); + } +} \ No newline at end of file diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java index 19ec275..08a8373 100644 --- a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryHookTest.java @@ -2,7 +2,10 @@ import com.atlassian.bitbucket.concurrent.BucketedExecutor; import com.atlassian.bitbucket.concurrent.ConcurrencyService; +import com.atlassian.bitbucket.event.repository.RepositoryDeletedEvent; +import com.atlassian.bitbucket.event.repository.RepositoryModifiedEvent; import com.atlassian.bitbucket.hook.repository.*; +import com.atlassian.bitbucket.i18n.I18nService; import com.atlassian.bitbucket.project.Project; import com.atlassian.bitbucket.repository.Repository; import com.atlassian.bitbucket.scm.git.GitScm; @@ -11,6 +14,8 @@ import com.atlassian.bitbucket.server.ApplicationPropertiesService; import com.atlassian.bitbucket.setting.Settings; import com.atlassian.bitbucket.setting.SettingsValidationErrors; +import com.atlassian.utils.process.StringOutputHandler; +import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -44,6 +49,8 @@ public class MirrorRepositoryHookTest { 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"; + private final String restApiURL = "http://bitbucket-mirror.englishtown.com/api"; + private final String privateToken = "123ASD"; @Mock private BucketedExecutor bucketedExecutor; @@ -53,12 +60,18 @@ public class MirrorRepositoryHookTest { private ConcurrencyService concurrencyService; private MirrorRepositoryHook hook; @Mock + private MirrorRemoteAdmin mirrorRemoteAdmin; + @Mock + private I18nService i18nService; + @Mock private PasswordEncryptor passwordEncryptor; @Mock private ApplicationPropertiesService propertiesService; @Captor private ArgumentCaptor requestCaptor; @Mock + private RepositoryHookService repositoryHookService; + @Mock private SettingsReflectionHelper settingsReflectionHelper; @Before @@ -68,8 +81,8 @@ public void setup() { 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); + hook = new MirrorRepositoryHook(concurrencyService, i18nService, passwordEncryptor, + propertiesService, bucketProcessor, mirrorRemoteAdmin, repositoryHookService, settingsReflectionHelper); } @Test @@ -126,7 +139,18 @@ public void testUnwantedEventsIgnored() { } @Test - public void testValidate() { + public void testValidateRepository() { + Repository repo = mock(Repository.class); + Scope scope = Scopes.repository(repo); + testValidate(scope); + } + @Test + public void testValidateProject() { + Project project = mock(Project.class); + Scope scope = Scopes.project(project); + testValidate(scope); + } + public void testValidate(Scope scope) { Settings settings = mock(Settings.class); Map map = new HashMap<>(); @@ -161,8 +185,6 @@ public void testValidate() { .thenReturn("+refs/heads/master:refs/heads/master") .thenReturn(""); - Repository repo = mock(Repository.class); - Scope scope = Scopes.repository(repo); SettingsValidationErrors errors; errors = mock(SettingsValidationErrors.class); @@ -234,7 +256,124 @@ public void testValidateForProject() { hook.validate(settings, errors, Scopes.project(project)); - verifyZeroInteractions(bucketedExecutor, errors, settings); + verifyZeroInteractions(bucketedExecutor); + } + + @Test + public void testRepositoryDeleted() { + Repository repo = mock(Repository.class); + when(repo.getName()).thenReturn("test"); + +// RepositoryDeletedEvent deletedEvent = mock(RepositoryDeletedEvent.class, withSettings().verboseLogging()); + RepositoryDeletedEvent deletedEvent = mock(RepositoryDeletedEvent.class); + when(deletedEvent.getRepository()).thenReturn(repo); + + RepositoryHookSettings repositoryHookSettings = mock(RepositoryHookSettings.class); + + Settings settings = mock(Settings.class); + when(repositoryHookSettings.getSettings()).thenReturn(settings); + + when(repositoryHookService.getSettings(any(GetRepositoryHookSettingsRequest.class))).thenReturn(repositoryHookSettings); + doThrow(new RuntimeException("Repository not found")).when(mirrorRemoteAdmin).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + hook.repositoryDeleted(deletedEvent); + verify(mirrorRemoteAdmin, times(0)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + + settings = defaultSettings(); + when(repositoryHookSettings.getSettings()).thenReturn(settings); + + hook.repositoryDeleted(deletedEvent); + verify(mirrorRemoteAdmin, times(1)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + } + + @Test + public void testRepositoryModified() { + Repository oldRepo = mock(Repository.class); + when(oldRepo.getName()).thenReturn("test"); + Project oldProject = mock(Project.class); + when(oldProject.getKey()).thenReturn("PROJECT"); + when(oldRepo.getProject()).thenReturn(oldProject); + + Repository newRepo = mock(Repository.class); + when(newRepo.getName()).thenReturn("test"); + Project newProject = mock(Project.class); + when(newProject.getKey()).thenReturn("PROJECT"); + when(newRepo.getProject()).thenReturn(newProject); + + RepositoryModifiedEvent modifiedEvent = mock(RepositoryModifiedEvent.class); + when(modifiedEvent.getRepository()).thenReturn(newRepo); + when(modifiedEvent.getOldValue()).thenReturn(oldRepo); + when(modifiedEvent.getNewValue()).thenReturn(newRepo); + + RepositoryHookSettings repositoryHookSettings = mock(RepositoryHookSettings.class); + + Settings settings = defaultSettings(); + when(repositoryHookSettings.getSettings()).thenReturn(settings); + + when(repositoryHookService.getSettings(any(GetRepositoryHookSettingsRequest.class))).thenReturn(repositoryHookSettings); + doThrow(new RuntimeException("Repository not found")).when(mirrorRemoteAdmin).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + doThrow(new RuntimeException("Repository no created")).when(bucketProcessor).runMirrorCommand(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + hook.repositoryModified(modifiedEvent); + verify(mirrorRemoteAdmin, times(0)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + verify(bucketProcessor, times(0)).runMirrorCommand(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + + when(newRepo.getName()).thenReturn("test"); + when(newProject.getKey()).thenReturn("NEW_PROJECT"); + hook.repositoryModified(modifiedEvent); + verify(mirrorRemoteAdmin, times(1)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + verify(bucketProcessor, times(1)).runMirrorCommand(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + + when(newRepo.getName()).thenReturn("newtest"); + when(newProject.getKey()).thenReturn("PROJECT"); + hook.repositoryModified(modifiedEvent); + verify(mirrorRemoteAdmin, times(2)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + verify(bucketProcessor, times(2)).runMirrorCommand(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + + when(newRepo.getName()).thenReturn("newtest"); + when(newProject.getKey()).thenReturn("NEWPROJECT"); + hook.repositoryModified(modifiedEvent); + verify(mirrorRemoteAdmin, times(3)).delete(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + verify(bucketProcessor, times(3)).runMirrorCommand(any(MirrorSettings.class), any(Repository.class), any(StringOutputHandler.class)); + + //TODO test with different settings + } + + @Test + public void testInterpolateMirrorRepoUrl() { + Repository repo = mock(Repository.class); + when(repo.getName()).thenReturn("test"); + Project project = mock(Project.class); + when(project.getKey()).thenReturn("PROJECT"); + when(repo.getProject()).thenReturn(project); + assertEquals("http://host/PROJECT/test.git", hook.interpolateMirrorRepoUrl(repo, "http://host/${repository.getProject().getKey()}/${repository.getName()}.git")); + try { + hook.interpolateMirrorRepoUrl(repo, "http://host/${.something()}/${repository.getName()}.git"); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to interpolate expression ${.something()} java.lang.NoSuchFieldException: Object not referenced",e.getMessage() ); + } + try { + hook.interpolateMirrorRepoUrl(repo, "http://host/${getProject().getKey()}/${repository.getName()}.git"); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to interpolate expression ${getProject().getKey()} java.lang.NoSuchFieldException: Unknown object getProject",e.getMessage()); + } + try { + hook.interpolateMirrorRepoUrl(repo, "http://host/${norepo.getProject().getKey()}/${repository.getName()}.git"); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to interpolate expression ${norepo.getProject().getKey()} java.lang.NoSuchFieldException: Unknown object norepo",e.getMessage()); + } + try { + hook.interpolateMirrorRepoUrl(repo, "http://host/${repository.getProject().getKey()}/${repository.getSomething()}.git"); + } catch (RuntimeException e) { + Assert.assertTrue(e.getMessage().startsWith("Failed to interpolate expression ${repository.getSomething()} java.lang.NoSuchMethodException:")); + } + try { + hook.interpolateMirrorRepoUrl(repo, "http://host/${repository.field}.git"); + Assert.fail("Exception not thrown"); + } catch (RuntimeException e) { + assertEquals("Failed to interpolate expression ${repository.field} java.lang.NoSuchMethodException: Not repository method specified",e.getMessage()); + } } private PostRepositoryHookContext buildContext() { @@ -266,6 +405,8 @@ private Settings defaultSettings() { 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); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_REST_API_URL), eq(""))).thenReturn(restApiURL); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PRIVATE_TOKEN), eq(""))).thenReturn(privateToken); return settings; } diff --git a/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryServletTest.java b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryServletTest.java new file mode 100644 index 0000000..c008a59 --- /dev/null +++ b/src/test/java/com/englishtown/bitbucket/hook/MirrorRepositoryServletTest.java @@ -0,0 +1,191 @@ +package com.englishtown.bitbucket.hook; + +import com.atlassian.bitbucket.hook.repository.RepositoryHookService; +import com.atlassian.bitbucket.i18n.I18nService; +import com.atlassian.bitbucket.project.Project; +import com.atlassian.bitbucket.repository.Repository; +import com.atlassian.bitbucket.repository.RepositoryService; +import com.atlassian.bitbucket.setting.Settings; +import com.atlassian.soy.renderer.SoyException; +import com.atlassian.soy.renderer.SoyTemplateRenderer; +import com.atlassian.utils.process.StringOutputHandler; +import org.junit.Assert; +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 javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.*; + +//import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.*; +/** + * Unit tests for {@link MirrorRepositoryServlet}. + */ + +public class MirrorRepositoryServletTest { + 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"; + private final String restApiURL = "http://bitbucket-mirror.englishtown.com/api"; + private final String privateToken = "123ASD"; + + @Rule + public MockitoRule mockitoRule = MockitoJUnit.rule().silent(); + + @Mock + private I18nService i18nService; + @Mock + private MirrorBucketProcessor pushProcessor; + @Mock + private MirrorRemoteAdmin mirrorRemoteAdmin; + @Mock + private MirrorRepositoryHook mirrorRepositoryHook; + @Mock + private RepositoryHookService repositoryHookService; + @Mock + private RepositoryService repositoryService; + @Mock + private SoyTemplateRenderer soyTemplateRenderer; + @Captor + private ArgumentCaptor> templateData; + @Mock + private HttpServletRequest req; + @Mock + private HttpServletResponse resp; + @Mock + private Repository repo; + + private MirrorRepositoryServlet mirrorRepositoryServlet; + + @Before + public void setup() { + + mirrorRepositoryServlet = new MirrorRepositoryServlet( + i18nService, + pushProcessor, + mirrorRemoteAdmin, + mirrorRepositoryHook, + repositoryService, + soyTemplateRenderer); + + when(repo.getName()).thenReturn("test"); + Project project = mock(Project.class); + when(project.getKey()).thenReturn("PROJECT"); + when(repo.getProject()).thenReturn(project); + when(repositoryService.getBySlug(eq("PROJECT"),eq("test"))).thenReturn(repo); + + } + + @Test + public void testDoGetWrongConfig() throws IOException, ServletException, SoyException { + doNothing().when(resp).sendError(eq(HttpServletResponse.SC_NOT_FOUND)); + + when(req.getPathInfo()).thenReturn("wrongPath"); + mirrorRepositoryServlet.doGet(req,resp); + verifyZeroInteractions(repositoryService); + + when(req.getPathInfo()).thenReturn("projects/PROJECT/repos/unknown_repo"); + mirrorRepositoryServlet.doGet(req,resp); + verify(repositoryService,times(1)).getBySlug(ArgumentMatchers.any(String.class),ArgumentMatchers.any(String.class)); + verifyZeroInteractions(repositoryHookService); + } + + @SuppressWarnings("unchecked") + @Test + public void testDoGet() throws IOException, ServletException, SoyException { + Settings settings=defaultSettings(); + when(mirrorRepositoryHook.getPluginSettings(repo)).thenReturn(settings); + when(mirrorRepositoryHook.interpolateMirrorRepoUrl(ArgumentMatchers.any(Repository.class),eq(mirrorRepoUrlHttp))).thenReturn(mirrorRepoUrlHttp); + doThrow(new RuntimeException("Object not referenced")).when(mirrorRepositoryHook).interpolateMirrorRepoUrl(ArgumentMatchers.any(Repository.class),eq("wrongUrl")); + + when(req.getMethod()).thenReturn("GET"); + when(req.getPathInfo()).thenReturn("projects/PROJECT/repos/test"); + mirrorRepositoryServlet.doGet(req,resp); + verify(soyTemplateRenderer, times(1)).render(eq(null), + eq("com.englishtown.stash-hook-mirror:mirror-hook-action-form"), + eq("com.englishtown.bitbucket.hook.action"), templateData.capture()); + + templateData.getValue().forEach((k, v) -> System.out.println("data " + k + " : " + v)); + System.out.println(templateData.getValue().keySet()); + Assert.assertTrue(templateData.getValue().containsKey("config")); + Assert.assertTrue(templateData.getValue().containsKey("errors")); + Assert.assertTrue(templateData.getValue().containsKey("repository")); + Assert.assertEquals(templateData.getValue().get("repository"),repo); + Assert.assertTrue(((Map)templateData.getValue().get("config")).containsKey("mirrorRepoUrl0")); + Assert.assertTrue(((Map)templateData.getValue().get("config")).containsKey("mirrorRepoUrl1")); + Assert.assertTrue(((Map)templateData.getValue().get("errors")).containsKey("mirrorRepoUrl1")); + } + @Test + public void testDoPost() throws IOException, ServletException { + Settings settings=defaultSettings(); + when(mirrorRepositoryHook.getPluginSettings(repo)).thenReturn(settings); + when(mirrorRepositoryHook.interpolateMirrorRepoUrl(ArgumentMatchers.any(Repository.class),eq(mirrorRepoUrlHttp))).thenReturn(mirrorRepoUrlHttp); + doThrow(new RuntimeException("Object not referenced")).when(mirrorRepositoryHook).interpolateMirrorRepoUrl(ArgumentMatchers.any(Repository.class),eq("wrongUrl")); + MirrorSettings ms0=new MirrorSettings(); + MirrorSettings ms1=new MirrorSettings(); + List mirrorSettingsList=new ArrayList<>(Arrays.asList(ms0,ms1)); + ms0.mirrorRepoUrl=mirrorRepoUrlHttp; + ms0.restApiURL=restApiURL; + ms0.password=password; + ms0.privateToken=privateToken; + ms0.suffix="0"; + ms1.mirrorRepoUrl="wrongUrl"; + ms1.restApiURL=restApiURL; + ms1.password=password; + ms1.privateToken=privateToken; + ms1.suffix="1"; + when(mirrorRepositoryHook.getMirrorSettings(any(Settings.class))).thenReturn(mirrorSettingsList); + + when(req.getMethod()).thenReturn("POST"); + when(req.getPathInfo()).thenReturn("projects/PROJECT/repos/test"); + mirrorRepositoryServlet.doPost(req,resp); + verify(mirrorRemoteAdmin,times(0)).delete(any(MirrorSettings.class),any(Repository.class),any(StringOutputHandler.class)); + verify(pushProcessor,times(0)).runMirrorCommand(any(MirrorSettings.class),any(Repository.class),any(PasswordHandler.class)); + + doThrow(new RuntimeException("Cannot delete")).when(mirrorRemoteAdmin).delete(eq (ms0),any(Repository.class),any(StringOutputHandler.class)); + when(req.getParameter( eq("delete0" ))).thenReturn("Anything"); + when(req.getParameter( eq("delete1" ))).thenReturn("Anything"); + when(req.getParameter( eq("trigger0" ))).thenReturn("Anything"); + when(req.getParameter( eq("trigger1" ))).thenReturn("Anything"); + mirrorRepositoryServlet.doPost(req,resp); + verify(mirrorRemoteAdmin,times(2)).delete(any(MirrorSettings.class),any(Repository.class),any(StringOutputHandler.class)); + verify(pushProcessor,times(1)).runMirrorCommand(any(MirrorSettings.class),any(Repository.class),any(StringOutputHandler.class)); + } + private Settings defaultSettings() { + Map map = new HashMap<>(); + map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL+"0", mirrorRepoUrlHttp); + map.put(MirrorRepositoryHook.SETTING_PASSWORD+"0", password); + map.put(MirrorRepositoryHook.SETTING_REST_API_URL+"0", restApiURL); + map.put(MirrorRepositoryHook.SETTING_PRIVATE_TOKEN+"0", privateToken); + map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL+"1", "wrongUrl"); + map.put(MirrorRepositoryHook.SETTING_PASSWORD+"1", password); + map.put(MirrorRepositoryHook.SETTING_REST_API_URL+"1", restApiURL); + map.put(MirrorRepositoryHook.SETTING_PRIVATE_TOKEN+"1", privateToken); + + Settings settings = mock(Settings.class); + when(settings.asMap()).thenReturn(map); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL+"0"), eq(""))).thenReturn(mirrorRepoUrlHttp); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL+"0"))).thenReturn(mirrorRepoUrlHttp); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD+"0"), eq(""))).thenReturn(password); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD+"0"))).thenReturn(password); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_REST_API_URL+"0"), eq(""))).thenReturn(restApiURL); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_REST_API_URL+"0"))).thenReturn(restApiURL); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PRIVATE_TOKEN+"0"), eq(""))).thenReturn(privateToken); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PRIVATE_TOKEN+"0"))).thenReturn(privateToken); + + return settings; + } + +} diff --git a/src/test/java/com/englishtown/bitbucket/hook/PasswordHandlerTest.java b/src/test/java/com/englishtown/bitbucket/hook/PasswordHandlerTest.java index 46449f5..1b31227 100644 --- a/src/test/java/com/englishtown/bitbucket/hook/PasswordHandlerTest.java +++ b/src/test/java/com/englishtown/bitbucket/hook/PasswordHandlerTest.java @@ -14,15 +14,16 @@ public class PasswordHandlerTest { @SuppressWarnings("FieldCanBeLocal") private final String password = "pwd@123"; - private final String secretText = "https://test.user:pwd@123@test.englishtown.com/scm/test/test.git"; - private final String cleanedText = "https://test.user:*****@test.englishtown.com/scm/test/test.git"; + private final String privateToken = "TOKENDATA"; + private final String secretText = "https://test.user:pwd@123@test.englishtown.com/scm/test/test.git?private_token=TOKENDATA"; + private final String cleanedText = "https://test.user:*****@test.englishtown.com/scm/test/test.git?private_token=*****"; private CommandExitHandler exitHandler; private PasswordHandler handler; @Before public void setup() throws Exception { exitHandler = mock(CommandExitHandler.class); - handler = new PasswordHandler(password, exitHandler); + handler = new PasswordHandler(password, privateToken, exitHandler); } @Test diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml new file mode 100644 index 0000000..90d979e --- /dev/null +++ b/src/test/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d %5p | %t | %-55logger{55} | %m %n + + + + + + + \ No newline at end of file