From c29ed9183697ee9a33c22f098819f04b1ac374f5 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez Date: Fri, 10 May 2013 17:50:40 -0400 Subject: [PATCH 1/3] Added support for multiple remote mirror repos --- README.md | 9 +- .../hook/DefaultSettingsReflectionHelper.java | 37 ++++ .../stash/hook/MirrorRepositoryHook.java | 167 +++++++++++------- .../stash/hook/PasswordEncryptor.java | 23 +++ .../stash/hook/SettingsReflectionHelper.java | 20 +++ src/main/resources/atlassian-plugin.xml | 3 + .../static/mirror-repository-hook.css | 3 + .../static/mirror-repository-hook.js | 43 +++++ .../static/mirror-repository-hook.soy | 105 +++++++---- .../stash/hook/MirrorRepositoryHookTest.java | 31 +++- 10 files changed, 329 insertions(+), 112 deletions(-) create mode 100644 src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java create mode 100644 src/main/java/com/englishtown/stash/hook/SettingsReflectionHelper.java create mode 100644 src/main/resources/static/mirror-repository-hook.css create mode 100644 src/main/resources/static/mirror-repository-hook.js diff --git a/README.md b/README.md index c816ef9..44bc969 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,12 @@ The following is a plugin for Atlassian Stash to provide repository mirroring to a remote repository. -* atlas-run -- installs this plugin into the product and starts it on localhost -* atlas-debug -- same as atlas-run, but allows a debugger to attach at port 5005 -* atlas-cli -- after atlas-run or atlas-debug, opens a Maven command line window: +* `atlas-run` -- installs this plugin into the product and starts it on localhost +* `atlas-debug` -- same as `atlas-run`, but allows a debugger to attach at port 5005 +* `atlas-debug --jvm-debug-suspend` -- same as `atlas-debug` but waits for the debugger to attach +* `atlas-cli` -- after atlas-run or atlas-debug, opens a Maven command line window: - 'pi' reinstalls the plugin into the running product instance -* atlas-help -- prints description for all commands in the SDK +* `atlas-help` -- prints description for all commands in the SDK Full documentation is always available at: diff --git a/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java b/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java new file mode 100644 index 0000000..01fadfd --- /dev/null +++ b/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java @@ -0,0 +1,37 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.setting.Settings; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +/** + * Default implementation of {@link SettingsReflectionHelper} + */ +public class DefaultSettingsReflectionHelper implements SettingsReflectionHelper { + + /** + * Set the values field of the {@link Settings} via reflection + * + * @param values the values to set + * @param settings the settings to set the values field on + */ + @Override + public void set(Map values, Settings settings) { + + try { + Field field = settings.getClass().getDeclaredField("values"); + field.setAccessible(true); + field.set(settings, values); + + } catch (NoSuchFieldException e) { + throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " + + "hook.", e); + } catch (IllegalAccessException e) { + throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " + + "hook.", e); + } + + } +} diff --git a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java index 484d923..fe7c40a 100644 --- a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java @@ -21,15 +21,20 @@ import javax.annotation.Nonnull; import java.lang.reflect.Field; import java.net.URI; import java.net.URISyntaxException; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { + static class MirrorSettings { + String mirrorRepoUrl; + String username; + String password; + String suffix; + } + public static final String PLUGIN_SETTINGS_KEY = "com.englishtown.stash.hook.mirror"; static final String SETTING_MIRROR_REPO_URL = "mirrorRepoUrl"; static final String SETTING_USERNAME = "username"; @@ -40,6 +45,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep private final I18nService i18nService; private final ScheduledExecutorService executor; private final PasswordEncryptor passwordEncryptor; + private final SettingsReflectionHelper settingsReflectionHelper; private static final Logger logger = LoggerFactory.getLogger(MirrorRepositoryHook.class); public MirrorRepositoryHook( @@ -47,6 +53,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep I18nService i18nService, ScheduledExecutorService executor, PasswordEncryptor passwordEncryptor, + SettingsReflectionHelper settingsReflectionHelper, PluginSettingsFactory pluginSettingsFactory ) { logger.debug("MirrorRepositoryHook: init started"); @@ -56,6 +63,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep this.i18nService = i18nService; this.executor = executor; this.passwordEncryptor = passwordEncryptor; + this.settingsReflectionHelper = settingsReflectionHelper; // Init password encryptor PluginSettings pluginSettings = pluginSettingsFactory.createSettingsForKey(PLUGIN_SETTINGS_KEY); @@ -82,20 +90,19 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep logger.debug("MirrorRepositoryHook: postReceive started."); - Settings settings = context.getSettings(); - String mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL); - String username = settings.getString(SETTING_USERNAME); - String password = settings.getString(SETTING_PASSWORD); + List mirrorSettings = getMirrorSettings(context.getSettings()); - runMirrorCommand(mirrorRepoUrl, username, password, context.getRepository()); + for (MirrorSettings settings : mirrorSettings) { + runMirrorCommand(settings, context.getRepository()); + } } - void runMirrorCommand(String mirrorRepoUrl, String username, final String encryptedPassword, final Repository repository) { + void runMirrorCommand(MirrorSettings settings, final Repository repository) { try { - final String password = passwordEncryptor.decrypt(encryptedPassword); - final URI authenticatedUrl = getAuthenticatedUrl(mirrorRepoUrl, username, password); + final String password = passwordEncryptor.decrypt(settings.password); + final URI authenticatedUrl = getAuthenticatedUrl(settings.mirrorRepoUrl, settings.username, password); executor.submit(new Callable() { @@ -164,46 +171,24 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep @Nonnull Repository repository) { try { - int count = 0; + boolean ok = true; logger.debug("MirrorRepositoryHook: validate started."); - String mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL, ""); - if (mirrorRepoUrl.isEmpty()) { - count++; - errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url is required."); - } else { - URI uri; - try { - uri = URI.create(mirrorRepoUrl); - if (!uri.getScheme().toLowerCase().startsWith("http") || mirrorRepoUrl.contains("@")) { - count++; - errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url must be a valid http(s) " + - "URI and the user should be specified separately."); - } - } catch (Exception ex) { - count++; - errors.addFieldError(SETTING_MIRROR_REPO_URL, "The mirror repo url must be a valid http(s) URI."); - } - } + List mirrorSettings = getMirrorSettings(settings); - String username = settings.getString(SETTING_USERNAME, ""); - if (username.isEmpty()) { - count++; - errors.addFieldError(SETTING_USERNAME, "The username is required."); - } - - String password = settings.getString(SETTING_PASSWORD, ""); - if (password.isEmpty()) { - count++; - errors.addFieldError(SETTING_PASSWORD, "The password is required."); + for (MirrorSettings ms : mirrorSettings) { + if (!validate(ms, settings, errors)) { + ok = false; + } } // If no errors, run the mirror command - if (count == 0) { - encryptPassword(settings, SETTING_PASSWORD, password); - runMirrorCommand(mirrorRepoUrl, username, password, repository); + if (ok) { + updateSettings(mirrorSettings, settings); + for (MirrorSettings ms : mirrorSettings) { + runMirrorCommand(ms, repository); + } } - logger.debug("MirrorRepositoryHook: validate completed with {} error(s).", count); } catch (Exception e) { logger.error("Error running MirrorRepositoryHook validate.", e); @@ -212,35 +197,81 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep } - void encryptPassword(Settings settings, String settingName, String password) { + List getMirrorSettings(Settings settings) { + + List results = new ArrayList(); + Map allSettings = settings.asMap(); + int count = 0; + + for (String key : allSettings.keySet()) { + if (key.startsWith(SETTING_MIRROR_REPO_URL)) { + String suffix = key.substring(SETTING_MIRROR_REPO_URL.length()); + + MirrorSettings ms = new MirrorSettings(); + ms.mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL + suffix, ""); + ms.username = settings.getString(SETTING_USERNAME + suffix, ""); + ms.password = settings.getString(SETTING_PASSWORD + suffix, ""); + ms.suffix = count > 0 ? String.valueOf(count) : ""; + results.add(ms); + + count++; + } + } + + return results; + } + + boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) { + + boolean result = true; + + if (ms.mirrorRepoUrl.isEmpty()) { + result = false; + errors.addFieldError(SETTING_MIRROR_REPO_URL + ms.suffix, "The mirror repo url is required."); + } else { + URI uri; + try { + uri = URI.create(ms.mirrorRepoUrl); + if (!uri.getScheme().toLowerCase().startsWith("http") || ms.mirrorRepoUrl.contains("@")) { + result = false; + errors.addFieldError(SETTING_MIRROR_REPO_URL + ms.suffix, + "The mirror repo url must be a valid http(s) URI and the username " + + "should be specified separately."); + } + } catch (Exception ex) { + result = false; + errors.addFieldError(SETTING_MIRROR_REPO_URL + ms.suffix, + "The mirror repo url must be a valid http(s) URI."); + } + } - // Skip if already encrypted - if (passwordEncryptor.isEncrypted(password)) { - return; + if (ms.username.isEmpty()) { + result = false; + errors.addFieldError(SETTING_USERNAME + ms.suffix, "The username is required."); } - try { - // Unfortunately the settings are stored in an immutable map, so need to cheat with reflection - logger.info("Encrypting password"); - Field field = settings.getClass().getDeclaredField("values"); - field.setAccessible(true); - - // Create mutable copy of values, and encrypt the password - @SuppressWarnings("unchecked") - Map values = (Map) field.get(settings); - values = new HashMap(values); - values.put(settingName, passwordEncryptor.encrypt(password)); - - field.set(settings, values); - - } catch (NoSuchFieldException e) { - throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " + - "hook.", e); - } catch (IllegalAccessException e) { - throw new RuntimeException("Unable to encrypt the password. Check for an updated version of the mirror " + - "hook.", e); + if (ms.password.isEmpty()) { + result = false; + errors.addFieldError(SETTING_PASSWORD + ms.suffix, "The password is required."); + } + + return result; + } + + void updateSettings(List mirrorSettings, Settings settings) { + + Map values = new HashMap(); + + // Store each mirror setting + for (MirrorSettings ms : mirrorSettings) { + values.put(SETTING_MIRROR_REPO_URL + ms.suffix, ms.mirrorRepoUrl); + values.put(SETTING_USERNAME + ms.suffix, ms.username); + values.put(SETTING_PASSWORD + ms.suffix, passwordEncryptor.encrypt(ms.password)); } + // Unfortunately the settings are stored in an immutable map, so need to cheat with reflection + settingsReflectionHelper.set(values, settings); + } } \ No newline at end of file diff --git a/src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java b/src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java index 7b23ef6..4e61575 100644 --- a/src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java +++ b/src/main/java/com/englishtown/stash/hook/PasswordEncryptor.java @@ -7,12 +7,35 @@ import com.atlassian.sal.api.pluginsettings.PluginSettings; */ 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 + * + * @param password the password to check + * @return true if the password is encrypted + */ boolean isEncrypted(String password); + /** + * Encrypts the password if it is not already encrypted + * + * @param password the password to encrypt + * @return encrypted password + */ String encrypt(String password); + /** + * Decrypts the password if it is encrypted + * + * @param password the encrypted password to decrypt + * @return the decrypted password + */ String decrypt(String password); } diff --git a/src/main/java/com/englishtown/stash/hook/SettingsReflectionHelper.java b/src/main/java/com/englishtown/stash/hook/SettingsReflectionHelper.java new file mode 100644 index 0000000..f7c57c6 --- /dev/null +++ b/src/main/java/com/englishtown/stash/hook/SettingsReflectionHelper.java @@ -0,0 +1,20 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.setting.Settings; + +import java.util.Map; + +/** + * Helper service to set {@link Settings} + */ +public interface SettingsReflectionHelper { + + /** + * Set the values field of the {@link Settings} via reflection + * + * @param values the values to set + * @param settings the settings to set the values field on + */ + void set(Map values, Settings settings); + +} diff --git a/src/main/resources/atlassian-plugin.xml b/src/main/resources/atlassian-plugin.xml index 5a4e77d..bdddb28 100644 --- a/src/main/resources/atlassian-plugin.xml +++ b/src/main/resources/atlassian-plugin.xml @@ -15,6 +15,9 @@ com.englishtown.stash.hook.PasswordEncryptor + + com.englishtown.stash.hook.SettingsReflectionHelper + diff --git a/src/main/resources/static/mirror-repository-hook.css b/src/main/resources/static/mirror-repository-hook.css new file mode 100644 index 0000000..553adaf --- /dev/null +++ b/src/main/resources/static/mirror-repository-hook.css @@ -0,0 +1,3 @@ +.et-remove-button { + margin-left: 5px !important; +} \ No newline at end of file diff --git a/src/main/resources/static/mirror-repository-hook.js b/src/main/resources/static/mirror-repository-hook.js new file mode 100644 index 0000000..a75f6cf --- /dev/null +++ b/src/main/resources/static/mirror-repository-hook.js @@ -0,0 +1,43 @@ +define('et/hook/mirror', ['jquery', 'exports'], function ($, exports) { + + exports.init = function (createSubView, createButton) { + $('#et-add-button').click(function () { + try { + var currIndex, index, name, html; + + // Determine the current index from the last password input + name = $('div.hook-config-contents input:password').last().attr("name"); + + if (name && name.length >= 8) { + currIndex = parseInt(name.substring(8)); + index = (isNaN(currIndex) ? 0 : currIndex) + 1; + } else { + index = ''; + } + + // Insert template after last field-group div + html = createSubView({index: index, config: {}, errors: {}}); + $('#et-add-button').before(html); + + addRemoveButton(); + + } catch (e) { + alert(e.message); + } + }); + + 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({name: 'et-remove-button', text: 'Remove', extraClasses: 'et-remove-button'}); + group.find('.et-mirror-repo input').after(html); + + group.find('.et-remove-button').click(function (e) { + $(e.currentTarget).parents('.et-mirror-group').remove(); + }) + } + + addRemoveButton(); + } + +}); \ No newline at end of file diff --git a/src/main/resources/static/mirror-repository-hook.soy b/src/main/resources/static/mirror-repository-hook.soy index 58a5926..8321ba9 100644 --- a/src/main/resources/static/mirror-repository-hook.soy +++ b/src/main/resources/static/mirror-repository-hook.soy @@ -5,40 +5,77 @@ * @param? errors */ {template .view} - {call aui.form.textField} - {param id: 'mirrorRepoUrl' /} - {param value: $config['mirrorRepoUrl'] /} - {param labelContent} - {stash_i18n('com.englishtown.stash.hook.mirror.strings.mirrorRepoUrl.label', 'Mirror Repo URL')} - {/param} - {param isRequired: true /} - {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.mirrorRepoUrl.description', - 'URL to the remote Stash mirrored repo') /} - {param extraClasses: 'long' /} - {param errorTexts: $errors ? $errors['mirrorRepoUrl'] : null /} - {/call} - {call aui.form.textField} - {param id: 'username' /} - {param value: $config['username'] /} - {param labelContent} - {stash_i18n('com.englishtown.stash.hook.mirror.strings.username.label', 'Username')} - {/param} - {param isRequired: true /} - {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.username.description', - 'The username to use for pushing to the mirror') /} - {param extraClasses: 'long' /} - {param errorTexts: $errors ? $errors['username'] : null /} + + // Always add one subview + {call .subview data="all"} + {param index: '' /} {/call} - {call aui.form.passwordField} - {param id: 'password' /} - {param value: $config['password'] /} - {param labelContent} - {stash_i18n('com.englishtown.stash.hook.mirror.strings.password.label', 'Password')} - {/param} - {param isRequired: true /} - {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.password.description', - 'The password to use for pushing to the mirror') /} - {param extraClasses: 'long' /} - {param errorTexts: $errors ? $errors['password'] : null /} + + // Add additional mirrors, up to 10 total + {foreach $index in [1, 2, 3, 4, 5, 6, 7, 8, 9]} + {if $config['mirrorRepoUrl' + $index] or $config['username' + $index]} + {call .subview data="all"} + {param index: $index /} + {/call} + {/if} + {/foreach} + + // Button to add additional mirrors + {call aui.form.button} + {param id: 'et-add-button' /} + {param text: 'Add Mirror' /} {/call} + + // Call init method with subview callback + {/template} + +/** + * @param config + * @param? errors + * @param index + */ +{template .subview} +
+ {call aui.form.textField} + {param id: 'mirrorRepoUrl' + $index /} + {param value: $config['mirrorRepoUrl' + $index] /} + {param labelContent} + {stash_i18n('com.englishtown.stash.hook.mirror.strings.mirrorRepoUrl.label', 'Mirror Repo URL')} + {/param} + {param isRequired: true /} + {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.mirrorRepoUrl.description', + 'URL to the remote Stash mirrored repo') /} + {param extraClasses: 'long et-mirror-repo' /} + {param errorTexts: $errors ? $errors['mirrorRepoUrl' + $index] : null /} + {/call} + {call aui.form.textField} + {param id: 'username' + $index /} + {param value: $config['username' + $index] /} + {param labelContent} + {stash_i18n('com.englishtown.stash.hook.mirror.strings.username.label', 'Username')} + {/param} + {param isRequired: true /} + {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.username.description', + 'The username to use for pushing to the mirror') /} + {param extraClasses: 'long' /} + {param errorTexts: $errors ? $errors['username' + $index] : null /} + {/call} + {call aui.form.passwordField} + {param id: 'password' + $index /} + {param value: $config['password' + $index] /} + {param labelContent} + {stash_i18n('com.englishtown.stash.hook.mirror.strings.password.label', 'Password')} + {/param} + {param isRequired: true /} + {param descriptionText: stash_i18n('com.englishtown.stash.hook.mirror.strings.password.description', + 'The password to use for pushing to the mirror') /} + {param extraClasses: 'long' /} + {param errorTexts: $errors ? $errors['password' + $index] : null /} + {/call} +
+{/template} + + diff --git a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java index f98dc16..b8f4da8 100644 --- a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java @@ -26,6 +26,8 @@ import org.mockito.runners.MockitoJUnitRunner; import java.net.URI; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -48,6 +50,8 @@ public class MirrorRepositoryHookTest { @Mock private PasswordEncryptor passwordEncryptor; @Mock + private SettingsReflectionHelper settingsReflectionHelper; + @Mock private PluginSettingsFactory pluginSettingsFactory; @Mock private PluginSettings pluginSettings; @@ -79,7 +83,8 @@ public class MirrorRepositoryHookTest { when(pluginSettingsFactory.createSettingsForKey(anyString())).thenReturn(pluginSettings); - hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, passwordEncryptor, pluginSettingsFactory); + hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, passwordEncryptor + , settingsReflectionHelper, pluginSettingsFactory); } @@ -88,10 +93,14 @@ public class MirrorRepositoryHookTest { when(passwordEncryptor.decrypt(anyString())).thenReturn(password); + Map map = new HashMap(); + map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL, ""); + Settings settings = mock(Settings.class); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL))).thenReturn(mirrorRepoUrl); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME))).thenReturn(username); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD))).thenReturn(password); + when(settings.asMap()).thenReturn(map); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))).thenReturn(mirrorRepoUrl); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME), eq(""))).thenReturn(username); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD), eq(""))).thenReturn(password); Repository repo = mock(Repository.class); when(repo.getName()).thenReturn("test"); @@ -111,8 +120,13 @@ public class MirrorRepositoryHookTest { GitScm gitScm = mock(GitScm.class); when(gitScm.getCommandBuilderFactory()).thenThrow(new RuntimeException("Intentional unit test exception")); - MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, passwordEncryptor, pluginSettingsFactory); - hook.runMirrorCommand(mirrorRepoUrl, username, password, mock(Repository.class)); + MirrorRepositoryHook hook = new MirrorRepositoryHook(gitScm, mock(I18nService.class), executor, + passwordEncryptor, settingsReflectionHelper, pluginSettingsFactory); + MirrorRepositoryHook.MirrorSettings ms = new MirrorRepositoryHook.MirrorSettings(); + ms.mirrorRepoUrl = mirrorRepoUrl; + ms.username = username; + ms.password = password; + hook.runMirrorCommand(ms, mock(Repository.class)); verify(executor).submit(argumentCaptor.capture()); Callable callable = argumentCaptor.getValue(); @@ -171,6 +185,11 @@ public class MirrorRepositoryHookTest { Settings settings = mock(Settings.class); + Map map = new HashMap(); + map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL, ""); + + when(settings.asMap()).thenReturn(map); + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))) .thenThrow(new RuntimeException("Intentional unit test exception")) .thenReturn("") From 44c289296471ac6afd9af1a558dc779bae0d1eeb Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez Date: Mon, 13 May 2013 16:17:14 -0400 Subject: [PATCH 2/3] Added unit tests --- pom.xml | 6 +++ .../hook/DefaultSettingsReflectionHelper.java | 1 - .../stash/hook/MirrorRepositoryHook.java | 13 +++--- .../DefaultSettingsReflectionHelperTest.java | 40 +++++++++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelperTest.java diff --git a/pom.xml b/pom.xml index 1aa2275..0a2984d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ atlassian-plugin + UTF-8 2.3.1 2.3.1 4.1.7 @@ -100,6 +101,11 @@ ${common-lang.version} + + com.atlassian.stash + stash-service-impl + test + com.atlassian.plugins atlassian-plugins-osgi-testrunner diff --git a/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java b/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java index 01fadfd..00110da 100644 --- a/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java +++ b/src/main/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelper.java @@ -3,7 +3,6 @@ package com.englishtown.stash.hook; import com.atlassian.stash.setting.Settings; import java.lang.reflect.Field; -import java.util.HashMap; import java.util.Map; /** diff --git a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java index fe7c40a..f6d1a4a 100644 --- a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java @@ -18,7 +18,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nonnull; -import java.lang.reflect.Field; import java.net.URI; import java.net.URISyntaxException; import java.util.*; @@ -28,7 +27,7 @@ import java.util.concurrent.TimeUnit; public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, RepositorySettingsValidator { - static class MirrorSettings { + protected static class MirrorSettings { String mirrorRepoUrl; String username; String password; @@ -147,7 +146,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep } } - URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { + protected URI getAuthenticatedUrl(String mirrorRepoUrl, String username, String password) throws URISyntaxException { URI uri = URI.create(mirrorRepoUrl); String userInfo = username + ":" + password; @@ -197,7 +196,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep } - List getMirrorSettings(Settings settings) { + protected List getMirrorSettings(Settings settings) { List results = new ArrayList(); Map allSettings = settings.asMap(); @@ -221,7 +220,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep return results; } - boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) { + protected boolean validate(MirrorSettings ms, Settings settings, SettingsValidationErrors errors) { boolean result = true; @@ -258,7 +257,7 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep return result; } - void updateSettings(List mirrorSettings, Settings settings) { + protected void updateSettings(List mirrorSettings, Settings settings) { Map values = new HashMap(); @@ -274,4 +273,4 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep } -} \ No newline at end of file +} diff --git a/src/test/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelperTest.java b/src/test/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelperTest.java new file mode 100644 index 0000000..94961aa --- /dev/null +++ b/src/test/java/com/englishtown/stash/hook/DefaultSettingsReflectionHelperTest.java @@ -0,0 +1,40 @@ +package com.englishtown.stash.hook; + +import com.atlassian.stash.internal.setting.MapSettingsBuilder; +import com.atlassian.stash.setting.Settings; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +/** + * Created with IntelliJ IDEA. + * User: adriangonzalez + * Date: 5/13/13 + * Time: 3:54 PM + * To change this template use File | Settings | File Templates. + */ +public class DefaultSettingsReflectionHelperTest { + + @Test + public void testSet() throws Exception { + + DefaultSettingsReflectionHelper helper = new DefaultSettingsReflectionHelper(); + MapSettingsBuilder builder = new MapSettingsBuilder(); + Map values = new HashMap(); + + builder.add("old", "old"); + values.put("new", "new"); + + Settings settings = builder.build(); + helper.set(values, settings); + + assertNull(settings.getString("old")); + assertEquals("new", settings.getString("new")); + + } + +} From e09a6ed6d64bdd5b3f4975fd48976a88618a89a1 Mon Sep 17 00:00:00 2001 From: Adrian Gonzalez Date: Mon, 13 May 2013 17:16:13 -0400 Subject: [PATCH 3/3] Changed settings to always have a numeric suffix starting with 0. --- .../stash/hook/MirrorRepositoryHook.java | 5 ++--- .../resources/static/mirror-repository-hook.js | 4 +--- .../static/mirror-repository-hook.soy | 2 +- .../stash/hook/MirrorRepositoryHookTest.java | 18 +++++++++--------- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java index f6d1a4a..7cd9636 100644 --- a/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java +++ b/src/main/java/com/englishtown/stash/hook/MirrorRepositoryHook.java @@ -210,10 +210,9 @@ public class MirrorRepositoryHook implements AsyncPostReceiveRepositoryHook, Rep ms.mirrorRepoUrl = settings.getString(SETTING_MIRROR_REPO_URL + suffix, ""); ms.username = settings.getString(SETTING_USERNAME + suffix, ""); ms.password = settings.getString(SETTING_PASSWORD + suffix, ""); - ms.suffix = count > 0 ? String.valueOf(count) : ""; - results.add(ms); + ms.suffix = String.valueOf(count++); - count++; + results.add(ms); } } diff --git a/src/main/resources/static/mirror-repository-hook.js b/src/main/resources/static/mirror-repository-hook.js index a75f6cf..8a1126c 100644 --- a/src/main/resources/static/mirror-repository-hook.js +++ b/src/main/resources/static/mirror-repository-hook.js @@ -3,7 +3,7 @@ define('et/hook/mirror', ['jquery', 'exports'], function ($, exports) { exports.init = function (createSubView, createButton) { $('#et-add-button').click(function () { try { - var currIndex, index, name, html; + var currIndex, index = 0, name, html; // Determine the current index from the last password input name = $('div.hook-config-contents input:password').last().attr("name"); @@ -11,8 +11,6 @@ define('et/hook/mirror', ['jquery', 'exports'], function ($, exports) { if (name && name.length >= 8) { currIndex = parseInt(name.substring(8)); index = (isNaN(currIndex) ? 0 : currIndex) + 1; - } else { - index = ''; } // Insert template after last field-group div diff --git a/src/main/resources/static/mirror-repository-hook.soy b/src/main/resources/static/mirror-repository-hook.soy index 8321ba9..6cf1d39 100644 --- a/src/main/resources/static/mirror-repository-hook.soy +++ b/src/main/resources/static/mirror-repository-hook.soy @@ -8,7 +8,7 @@ // Always add one subview {call .subview data="all"} - {param index: '' /} + {param index: 0 /} {/call} // Add additional mirrors, up to 10 total diff --git a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java index b8f4da8..1a4bdb6 100644 --- a/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java +++ b/src/test/java/com/englishtown/stash/hook/MirrorRepositoryHookTest.java @@ -186,22 +186,22 @@ public class MirrorRepositoryHookTest { Settings settings = mock(Settings.class); Map map = new HashMap(); - map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL, ""); + map.put(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0", ""); when(settings.asMap()).thenReturn(map); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), eq(""))) + when(settings.getString(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), eq(""))) .thenThrow(new RuntimeException("Intentional unit test exception")) .thenReturn("") .thenReturn("invalid uri") .thenReturn("http://should-not:have-user@stash-mirror.englishtown.com/scm/test/test.git") .thenReturn(mirrorRepoUrl); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME), eq(""))) + when(settings.getString(eq(MirrorRepositoryHook.SETTING_USERNAME + "0"), eq(""))) .thenReturn("") .thenReturn(username); - when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD), eq(""))) + when(settings.getString(eq(MirrorRepositoryHook.SETTING_PASSWORD + "0"), eq(""))) .thenReturn("") .thenReturn(password); @@ -215,20 +215,20 @@ public class MirrorRepositoryHookTest { errors = mock(SettingsValidationErrors.class); hook.validate(settings, errors, repo); verify(errors, never()).addFormError(anyString()); - verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); - verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_USERNAME), anyString()); - verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_PASSWORD), anyString()); + verify(errors).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); verify(errors, never()).addFormError(anyString()); - verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors).addFieldError(anyString(), anyString()); errors = mock(SettingsValidationErrors.class); hook.validate(settings, errors, repo); verify(errors, never()).addFormError(anyString()); - verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL), anyString()); + verify(errors).addFieldError(eq(MirrorRepositoryHook.SETTING_MIRROR_REPO_URL + "0"), anyString()); verify(errors).addFieldError(anyString(), anyString()); when(passwordEncryptor.isEncrypted(anyString())).thenReturn(true);