diff --git a/CHANGELOG.md b/CHANGELOG.md index 410d0f533e..145e2409df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Introduced merge detection for receive hooks ([#1278](https://github.com/scm-manager/scm-manager/pull/1278)) +- Check versions of plugin dependencies on plugin installation ([#1283](https://github.com/scm-manager/scm-manager/pull/1283)) ### Fixed - Repository names may not end with ".git" ([#1277](https://github.com/scm-manager/scm-manager/pull/1277)) diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java index faedb2a6f5..9d57c2a5dd 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPluginDescriptor.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- @@ -37,6 +37,7 @@ import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlElementWrapper; import javax.xml.bind.annotation.XmlRootElement; import java.util.Set; +import java.util.stream.Collectors; //~--- JDK imports ------------------------------------------------------------ @@ -67,7 +68,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin * @param condition * @param childFirstClassLoader * @param dependencies + * + * @deprecated this constructor uses dependencies with plain strings, + * which is deprecated because the version information is missing. + * This class should not instantiated manually, it is designed to be loaded by jaxb. */ + @Deprecated public InstalledPluginDescriptor(int scmVersion, PluginInformation information, PluginResources resources, PluginCondition condition, boolean childFirstClassLoader, Set dependencies, Set optionalDependencies) @@ -77,8 +83,17 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin this.resources = resources; this.condition = condition; this.childFirstClassLoader = childFirstClassLoader; - this.dependencies = dependencies; - this.optionalDependencies = optionalDependencies; + this.dependencies = mapToNameAndVersionSet(dependencies); + this.optionalDependencies = mapToNameAndVersionSet(optionalDependencies); + } + + private static Set mapToNameAndVersionSet(Set dependencies) { + if (dependencies == null){ + return ImmutableSet.of(); + } + return dependencies.stream() + .map(d -> new NameAndVersion(d, null)) + .collect(Collectors.toSet()); } //~--- methods -------------------------------------------------------------- @@ -173,13 +188,19 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin * @since 2.0.0 */ @Override - public Set getDependencies() - { - if (dependencies == null) - { + public Set getDependencies() { + return mapToStringSet(getDependenciesWithVersion()); + } + + /** + * Returns name and versions of the plugins which are this plugin depends on. + * @return dependencies with their versions + * @since 2.4.0 + */ + public Set getDependenciesWithVersion() { + if (dependencies == null) { dependencies = ImmutableSet.of(); } - return dependencies; } @@ -193,11 +214,18 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin */ @Override public Set getOptionalDependencies() { - if (optionalDependencies == null) - { + return mapToStringSet(getOptionalDependenciesWithVersion()); + } + + /** + * Returns name and versions of the plugins which are this plugin optional depends on. + * @return optional dependencies with their versions + * @since 2.4.0 + */ + public Set getOptionalDependenciesWithVersion() { + if (optionalDependencies == null) { optionalDependencies = ImmutableSet.of(); } - return optionalDependencies; } @@ -205,6 +233,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin return ImmutableSet.copyOf(Iterables.concat(getDependencies(), getOptionalDependencies())); } + private Set mapToStringSet(Set dependencies) { + return dependencies.stream() + .map(NameAndVersion::getName) + .collect(Collectors.toSet()); + } + /** * Method description * @@ -263,12 +297,12 @@ public final class InstalledPluginDescriptor extends ScmModule implements Plugin /** Field description */ @XmlElement(name = "dependency") @XmlElementWrapper(name = "dependencies") - private Set dependencies; + private Set dependencies; /** Field description */ @XmlElement(name = "dependency") @XmlElementWrapper(name = "optional-dependencies") - private Set optionalDependencies; + private Set optionalDependencies; /** Field description */ @XmlElement(name = "information") diff --git a/scm-core/src/main/java/sonia/scm/plugin/NameAndVersion.java b/scm-core/src/main/java/sonia/scm/plugin/NameAndVersion.java new file mode 100644 index 0000000000..414d3fcf98 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/NameAndVersion.java @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import com.google.common.base.Strings; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import sonia.scm.version.Version; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlValue; +import javax.xml.bind.annotation.adapters.XmlAdapter; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import java.util.Optional; + +/** + * @since 2.4.0 + */ +@Getter +@EqualsAndHashCode +@XmlAccessorType(XmlAccessType.FIELD) +public class NameAndVersion { + + @XmlValue + private String name; + + @XmlAttribute(name = "version") + @XmlJavaTypeAdapter(VersionXmlAdapter.class) + private Version version; + + NameAndVersion() { + // required for jaxb + } + + public NameAndVersion(String name) { + this(name, null); + } + + public NameAndVersion(String name, String version) { + this.name = name; + if (!Strings.isNullOrEmpty(version)) { + this.version = Version.parse(version); + } + } + + public Optional getVersion() { + return Optional.ofNullable(version); + } + + public Version mustGetVersion() { + if (version == null) { + throw new IllegalStateException("version is not set"); + } + return version; + } + + @Override + public String toString() { + return name + (version != null ? ":" + version.getParsedVersion() : ""); + } + + static class VersionXmlAdapter extends XmlAdapter { + + @Override + public Version unmarshal(String v) { + if (Strings.isNullOrEmpty(v)) { + return null; + } + return Version.parse(v); + } + + @Override + public String marshal(Version v) { + if (v != null) { + return v.getUnparsedVersion(); + } + return null; + } + } +} diff --git a/scm-core/src/main/java/sonia/scm/version/Version.java b/scm-core/src/main/java/sonia/scm/version/Version.java index c0190e6bd2..c6edb05daa 100644 --- a/scm-core/src/main/java/sonia/scm/version/Version.java +++ b/scm-core/src/main/java/sonia/scm/version/Version.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.version; //~--- non-JDK imports -------------------------------------------------------- @@ -268,11 +268,28 @@ public final class Version implements Comparable * * @return true if newer */ - public boolean isNewer(String versionString) - { - Version o = Version.parse(versionString); + public boolean isNewer(String versionString) { + return isNewer(Version.parse(versionString)); + } - return (o != null) && isNewer(o); + /** + * Returns true if the given version is newer or equal. + * @param versionString other version + * @return true if newer + * @since 2.4.0 + */ + public boolean isNewerOrEqual(String versionString) { + return isNewerOrEqual(Version.parse(versionString)); + } + + /** + * Returns true if the given version is newer or equal. + * @param o other version + * @return {@code true} if newer or equal + * @since 2.4.0 + */ + public boolean isNewerOrEqual(Version o) { + return compareTo(o) <= 0; } /** @@ -296,13 +313,31 @@ public final class Version implements Comparable * * @return true if older */ - public boolean isOlder(String versionString) - { - Version o = Version.parse(versionString); - - return (o != null) && isOlder(o); + public boolean isOlder(String versionString) { + return isOlder(Version.parse(versionString)); } + /** + * Returns true if the given version is older or equal. + * @param versionString other version + * @return {@code true} if older or equal + * @since 2.4.0 + */ + public boolean isOlderOrEqual(String versionString) { + return isOlderOrEqual(Version.parse(versionString)); + } + + /** + * Returns true if the given version is older or equal. + * @param o other version + * @return {@code true} if older or equal + * @since 2.4.0 + */ + public boolean isOlderOrEqual(Version o) { + return compareTo(o) >= 0; + } + + /** * Returns true if the version is a snapshot. * diff --git a/scm-core/src/test/java/sonia/scm/plugin/InstalledPluginDescriptorTest.java b/scm-core/src/test/java/sonia/scm/plugin/InstalledPluginDescriptorTest.java new file mode 100644 index 0000000000..ce78d659a5 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/plugin/InstalledPluginDescriptorTest.java @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import com.google.common.io.Resources; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.xml.bind.JAXB; +import java.net.URL; + +import static org.assertj.core.api.Assertions.assertThat; + +class InstalledPluginDescriptorTest { + + private static InstalledPluginDescriptor descriptor; + + @BeforeAll + @SuppressWarnings("UnstableApiUsage") + static void unmarshal() { + URL resource = Resources.getResource("sonia/scm/plugin/review-plugin.xml"); + descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class); + } + + @Test + void shouldUnmarshallDependencies() { + assertThat(descriptor.getDependencies()).containsOnly("scm-mail-plugin"); + assertThat(descriptor.getOptionalDependencies()).containsOnly("scm-editor-plugin", "scm-landingpage-plugin"); + assertThat(descriptor.getDependenciesInclusiveOptionals()).containsOnly("scm-mail-plugin", "scm-editor-plugin", "scm-landingpage-plugin"); + } + + @Test + void shouldUnmarshallDependenciesWithVersion() { + assertThat(descriptor.getDependenciesWithVersion()).containsOnly(new NameAndVersion("scm-mail-plugin", "2.1.0")); + assertThat(descriptor.getOptionalDependenciesWithVersion()).containsOnly( + new NameAndVersion("scm-landingpage-plugin", "1.0.0"), + new NameAndVersion("scm-editor-plugin") + ); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/version/VersionTest.java b/scm-core/src/test/java/sonia/scm/version/VersionTest.java index cd222cf57a..b58d355831 100644 --- a/scm-core/src/test/java/sonia/scm/version/VersionTest.java +++ b/scm-core/src/test/java/sonia/scm/version/VersionTest.java @@ -21,96 +21,77 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.version; -//~--- non-JDK imports -------------------------------------------------------- -import org.junit.Test; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ +import org.junit.jupiter.api.Test; import java.util.Arrays; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + /** - * * @author Sebastian Sdorra */ -public class VersionTest -{ +class VersionTest { - /** - * Method description - * - */ @Test - public void parseSimpleVersion() - { + void parseSimpleVersion() { Version v = Version.parse("1.0"); - assertTrue(v.getMajor() == 1); - assertTrue(v.getMinor() == 0); - assertTrue(v.getPatch() == 0); - assertFalse(v.isSnapshot()); - assertTrue(v.getType() == VersionType.RELEASE); - assertEquals(v.getParsedVersion(), "1.0.0"); + assertThat(v.getMajor()).isOne(); + assertThat(v.getMinor()).isZero(); + assertThat(v.getPatch()).isZero(); + assertThat(v.isSnapshot()).isFalse(); + assertThat(v.getType()).isSameAs(VersionType.RELEASE); + assertThat(v.getParsedVersion()).isEqualTo("1.0.0"); // test with snapshot v = Version.parse("1.1-SNAPSHOT"); - assertTrue(v.getMajor() == 1); - assertTrue(v.getMinor() == 1); - assertTrue(v.getPatch() == 0); - assertTrue(v.isSnapshot()); - assertTrue(v.getType() == VersionType.RELEASE); - assertEquals(v.getParsedVersion(), "1.1.0-SNAPSHOT"); + assertThat(v.getMajor()).isOne(); + assertThat(v.getMinor()).isOne(); + assertThat(v.getPatch()).isZero(); + assertThat(v.isSnapshot()).isTrue(); + assertThat(v.getType()).isSameAs(VersionType.RELEASE); + assertThat(v.getParsedVersion()).isEqualTo("1.1.0-SNAPSHOT"); // test with maintenance v = Version.parse("2.3.14"); - assertTrue(v.getMajor() == 2); - assertTrue(v.getMinor() == 3); - assertTrue(v.getPatch() == 14); - assertFalse(v.isSnapshot()); - assertTrue(v.getType() == VersionType.RELEASE); - assertEquals(v.getParsedVersion(), "2.3.14"); + assertThat(v.getMajor()).isEqualTo(2); + assertThat(v.getMinor()).isEqualTo(3); + assertThat(v.getPatch()).isEqualTo(14); + assertThat(v.isSnapshot()).isFalse(); + assertThat(v.getType()).isSameAs(VersionType.RELEASE); + assertThat(v.getParsedVersion()).isEqualTo("2.3.14"); } - /** - * Method description - * - */ @Test - public void parseTypeVersions() - { + void parseTypeVersions() { Version v = Version.parse("1.0-alpha"); - assertTrue(v.getMajor() == 1); - assertTrue(v.getMinor() == 0); - assertTrue(v.getPatch() == 0); - assertFalse(v.isSnapshot()); - assertTrue(v.getType() == VersionType.ALPHA); - assertTrue(v.getTypeVersion() == 1); - assertEquals(v.getParsedVersion(), "1.0.0-alpha1"); + assertThat(v.getMajor()).isOne(); + assertThat(v.getMinor()).isZero(); + assertThat(v.getPatch()).isZero(); + assertThat(v.isSnapshot()).isFalse(); + assertThat(v.getType()).isSameAs(VersionType.ALPHA); + assertThat(v.getTypeVersion()).isOne(); + assertThat(v.getParsedVersion()).isEqualTo("1.0.0-alpha1"); // Test release candidate v = Version.parse("2.1.2-RC3"); - assertTrue(v.getMajor() == 2); - assertTrue(v.getMinor() == 1); - assertTrue(v.getPatch() == 2); - assertFalse(v.isSnapshot()); - assertTrue(v.getType() == VersionType.RELEASE_CANDIDAT); - assertTrue(v.getTypeVersion() == 3); - assertEquals(v.getParsedVersion(), "2.1.2-RC3"); + assertThat(v.getMajor()).isEqualTo(2); + assertThat(v.getMinor()).isEqualTo(1); + assertThat(v.getPatch()).isEqualTo(2); + assertThat(v.isSnapshot()).isFalse(); + assertThat(v.getType()).isSameAs(VersionType.RELEASE_CANDIDAT); + assertThat(v.getTypeVersion()).isEqualTo(3); + assertThat(v.getParsedVersion()).isEqualTo("2.1.2-RC3"); } - /** - * Method description - * - */ @Test - public void testCompareTo() - { + void testCompareTo() { Version[] versions = new Version[9]; versions[0] = Version.parse("2.3.1-SNAPSHOT"); @@ -123,48 +104,45 @@ public class VersionTest versions[7] = Version.parse("2.3"); versions[8] = Version.parse("2.4.6"); Arrays.sort(versions); - assertEquals(versions[0].getParsedVersion(), "2.4.6"); - assertEquals(versions[1].getParsedVersion(), "2.3.1"); - assertEquals(versions[2].getParsedVersion(), "2.3.1-SNAPSHOT"); - assertEquals(versions[3].getParsedVersion(), "2.3.1-RC1"); - assertEquals(versions[4].getParsedVersion(), "2.3.1-beta2"); - assertEquals(versions[5].getParsedVersion(), "2.3.1-beta1"); - assertEquals(versions[6].getParsedVersion(), "2.3.1-alpha2"); - assertEquals(versions[7].getParsedVersion(), "2.3.1-M1"); - assertEquals(versions[8].getParsedVersion(), "2.3.0"); + assertThat(versions[0].getParsedVersion()).isEqualTo("2.4.6"); + assertThat(versions[1].getParsedVersion()).isEqualTo("2.3.1"); + assertThat(versions[2].getParsedVersion()).isEqualTo("2.3.1-SNAPSHOT"); + assertThat(versions[3].getParsedVersion()).isEqualTo("2.3.1-RC1"); + assertThat(versions[4].getParsedVersion()).isEqualTo("2.3.1-beta2"); + assertThat(versions[5].getParsedVersion()).isEqualTo("2.3.1-beta1"); + assertThat(versions[6].getParsedVersion()).isEqualTo("2.3.1-alpha2"); + assertThat(versions[7].getParsedVersion()).isEqualTo("2.3.1-M1"); + assertThat(versions[8].getParsedVersion()).isEqualTo("2.3.0"); } - /** - * Method description - * - */ @Test - public void testIsNewer() - { - assertFalse(Version.parse("1.0").isNewer("1.0.1")); - assertTrue(Version.parse("1.1").isNewer("1.1-alpha1")); - assertTrue(Version.parse("1.1").isNewer("1.1-RC5")); + void testIsNewer() { + assertThat(Version.parse("1.0").isNewer("1.0.1")).isFalse(); + assertThat(Version.parse("1.1").isNewer("1.1-alpha1")).isTrue(); + assertThat(Version.parse("1.1").isNewer("1.1-RC5")).isTrue(); } - /** - * Method description - * - */ @Test - public void testIsOlder() - { - assertFalse(Version.parse("1.0.1").isOlder("1.0")); - assertTrue(Version.parse("1.1-alpha1").isOlder("1.1")); - assertTrue(Version.parse("1.1-RC5").isOlder("1.1")); + void testIsOlder() { + assertThat(Version.parse("1.0.1").isOlder("1.0")).isFalse(); + assertThat(Version.parse("1.1-alpha1").isOlder("1.1")).isTrue(); + assertThat(Version.parse("1.1-RC5").isOlder("1.1")).isTrue(); } - /** - * Method description - * - */ - @Test(expected = VersionParseException.class) - public void testUnparseable() - { - Version.parse("aaaa"); + @Test + void testIsOlderOrEqual() { + assertThat(Version.parse("1.0.0").isOlderOrEqual("1.0.1")).isTrue(); + assertThat(Version.parse("1.0.1").isOlderOrEqual("1.0.1")).isTrue(); + } + + @Test + void testINewerOrEqual() { + assertThat(Version.parse("1.0.1").isNewerOrEqual("1.0.0")).isTrue(); + assertThat(Version.parse("1.0.1").isOlderOrEqual("1.0.1")).isTrue(); + } + + @Test + void testUnparseable() { + assertThrows(VersionParseException.class, () -> Version.parse("aaaa")); } } diff --git a/scm-core/src/test/resources/sonia/scm/plugin/review-plugin.xml b/scm-core/src/test/resources/sonia/scm/plugin/review-plugin.xml new file mode 100644 index 0000000000..dccadb5b12 --- /dev/null +++ b/scm-core/src/test/resources/sonia/scm/plugin/review-plugin.xml @@ -0,0 +1,74 @@ + + + + 2 + + Review + Cloudogu GmbH + Workflow + scm-review-plugin + 2.3.0-SNAPSHOT + Depict a review process with pull requests + + + 2.4.0-SNAPSHOT + + + + + + com.cloudogu.scm.review.emailnotification.EmailNotificationHook + com.cloudogu.scm.review.pullrequest.service.PullRequestRejectedEvent + + + Each {@link Rule} class implementation defines a type of workflow rule.<br> + <br> + Rules applied to your repositories are represented by {@link AppliedRule}s<br> + to support multiple {@link Rule}s of the same type with distinct configuration. + + true + true + com.cloudogu.scm.review.workflow.Rule + + + v2/pull-requests + com.cloudogu.scm.review.config.api.RepositoryConfigResource + + + com.cloudogu.scm.review.pullrequest.service.PullRequestEvent + + + com.cloudogu.scm.review.ProcessChangedFilesHook + + + scm-mail-plugin + + + scm-editor-plugin + scm-landingpage-plugin + + diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 69fc876704..0e1e044e83 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -42,6 +42,7 @@ import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -68,17 +69,30 @@ public class DefaultPluginManager implements PluginManager { private final Collection pendingUninstallQueue = new ArrayList<>(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); + private final Function, PluginInstallationContext> contextFactory; + @Inject public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus) { + this(loader, center, installer, restarter, eventBus, null); + } + + DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter, ScmEventBus eventBus, Function, PluginInstallationContext> contextFactory) { this.loader = loader; this.center = center; this.installer = installer; this.restarter = restarter; this.eventBus = eventBus; + if (contextFactory != null) { + this.contextFactory = contextFactory; + } else { + this.contextFactory = (availablePlugins -> PluginInstallationContext.from(getInstalled(), availablePlugins)); + } + this.computeInstallationDependencies(); } + @VisibleForTesting synchronized void computeInstallationDependencies() { loader.getInstalledPlugins() @@ -167,9 +181,10 @@ public class DefaultPluginManager implements PluginManager { List plugins = collectPluginsToInstall(name); List pendingInstallations = new ArrayList<>(); + for (AvailablePlugin plugin : plugins) { try { - PendingPluginInstallation pending = installer.install(plugin); + PendingPluginInstallation pending = installer.install(contextFactory.apply(plugins), plugin); dependencyTracker.addInstalled(plugin.getDescriptor()); pendingInstallations.add(pending); eventBus.post(new PluginEvent(PluginEvent.PluginEventType.INSTALLED, plugin)); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DependencyNotFoundException.java b/scm-webapp/src/main/java/sonia/scm/plugin/DependencyNotFoundException.java new file mode 100644 index 0000000000..7bfc335033 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DependencyNotFoundException.java @@ -0,0 +1,62 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class DependencyNotFoundException extends PluginInstallException { + + private final String plugin; + private final String missingDependency; + + public DependencyNotFoundException(String plugin, String missingDependency) { + super( + entity("Dependency", missingDependency) + .in("Plugin", plugin) + .build(), + String.format( + "missing dependency %s of plugin %s", + missingDependency, + plugin + ) + ); + this.plugin = plugin; + this.missingDependency = missingDependency; + } + + public String getPlugin() { + return plugin; + } + + public String getMissingDependency() { + return missingDependency; + } + + @Override + public String getCode() { + return "5GS6lwvWF1"; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DependencyVersionMismatchException.java b/scm-webapp/src/main/java/sonia/scm/plugin/DependencyVersionMismatchException.java new file mode 100644 index 0000000000..325c12fb86 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DependencyVersionMismatchException.java @@ -0,0 +1,60 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import lombok.Getter; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@Getter +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class DependencyVersionMismatchException extends PluginInstallException { + + private final String plugin; + private final String dependency; + private final String minVersion; + private final String currentVersion; + + public DependencyVersionMismatchException(String plugin, String dependency, String minVersion, String currentVersion) { + super( + entity("Dependency", dependency) + .in("Plugin", plugin) + .build(), + String.format( + "%s requires dependency %s at least in version %s, but it is installed in version %s", + plugin, dependency, minVersion, currentVersion + ) + ); + this.plugin = plugin; + this.dependency = dependency; + this.minVersion = minVersion; + this.currentVersion = currentVersion; + } + + @Override + public String getCode() { + return "E5S6niWwi1"; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java index 5007be1e75..777c4a11d9 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/ExplodedSmp.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- @@ -30,11 +30,10 @@ import com.google.common.base.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -//~--- JDK imports ------------------------------------------------------------ - import java.nio.file.Path; +import java.util.Objects; -import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ /** * The ExplodedSmp object represents an extracted SCM-Manager plugin. The object @@ -107,6 +106,25 @@ public final class ExplodedSmp return plugin; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExplodedSmp that = (ExplodedSmp) o; + return path.equals(that.path); + } + + @Override + public int hashCode() { + return Objects.hash(path); + } + + @Override + public String toString() { + PluginInformation information = plugin.getInformation(); + return information.getName() + "@" + information.getVersion() + " (" + path + ")"; + } + //~--- inner classes -------------------------------------------------------- /** diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java index af419fa297..ba83baef25 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginChecksumMismatchException.java @@ -26,6 +26,7 @@ package sonia.scm.plugin; import static sonia.scm.ContextEntry.ContextBuilder.entity; +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here public class PluginChecksumMismatchException extends PluginInstallException { public PluginChecksumMismatchException(AvailablePlugin plugin, String calculatedChecksum, String expectedChecksum) { super( diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInformationMismatchException.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInformationMismatchException.java new file mode 100644 index 0000000000..4d4639643e --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInformationMismatchException.java @@ -0,0 +1,51 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import lombok.Getter; + +import static sonia.scm.ContextEntry.ContextBuilder.entity; + +@Getter +@SuppressWarnings("squid:MaximumInheritanceDepth") // exceptions have a deep inheritance depth themselves; therefore we accept this here +public class PluginInformationMismatchException extends PluginInstallException { + + private final PluginInformation api; + private final PluginInformation downloaded; + + public PluginInformationMismatchException(PluginInformation api, PluginInformation downloaded, String message) { + super( + entity("Plugin", api.getName()).build(), + message + ); + this.api = api; + this.downloaded = downloaded; + } + + @Override + public String getCode() { + return "4RS6niPRX1"; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationContext.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationContext.java new file mode 100644 index 0000000000..f16da8bdeb --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationContext.java @@ -0,0 +1,73 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public final class PluginInstallationContext { + + private final Map dependencies; + + private PluginInstallationContext(Map dependencies) { + this.dependencies = dependencies; + } + + public static PluginInstallationContext empty() { + return new PluginInstallationContext(Collections.emptyMap()); + } + + public static PluginInstallationContext fromDescriptors(Iterable installed, Iterable pending) { + Map dependencies = new HashMap<>(); + appendDescriptors(dependencies, installed); + appendDescriptors(dependencies, pending); + return new PluginInstallationContext(dependencies); + } + + public static PluginInstallationContext from(Iterable installed, Iterable pending) { + Map dependencies = new HashMap<>(); + appendPlugins(dependencies, installed); + appendPlugins(dependencies, pending); + return new PluginInstallationContext(dependencies); + } + + private static

void appendDescriptors(Map dependencies, Iterable

descriptors) { + descriptors.forEach(desc -> appendPlugins(dependencies, desc.getInformation())); + } + + private static

void appendPlugins(Map dependencies, Iterable

plugins) { + plugins.forEach(plugin -> appendPlugins(dependencies, plugin.getDescriptor().getInformation())); + } + + private static void appendPlugins(Map dependencies, PluginInformation information) { + dependencies.put(information.getName(), new NameAndVersion(information.getName(), information.getVersion())); + } + + public Optional find(String name) { + return Optional.ofNullable(dependencies.get(name)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationVerifier.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationVerifier.java new file mode 100644 index 0000000000..1e2636b786 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstallationVerifier.java @@ -0,0 +1,104 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import sonia.scm.version.Version; + +import java.util.Optional; +import java.util.Set; + +public final class PluginInstallationVerifier { + + private final PluginInstallationContext context; + private final InstalledPluginDescriptor descriptor; + + private PluginInstallationVerifier(PluginInstallationContext context, InstalledPluginDescriptor descriptor) { + this.context = context; + this.descriptor = descriptor; + } + + public static void verify(PluginInstallationContext context, InstalledPlugin plugin) { + verify(context, plugin.getDescriptor()); + } + + public static void verify(PluginInstallationContext context, InstalledPluginDescriptor descriptor) { + new PluginInstallationVerifier(context, descriptor).doVerification(); + } + + private void doVerification() { + verifyConditions(); + verifyDependencies(); + verifyOptionalDependencies(); + } + + private void verifyConditions() { + // TODO we should provide more details here, which condition has failed + if (!descriptor.getCondition().isSupported()) { + throw new PluginConditionFailedException( + descriptor.getCondition(), + String.format( + "could not load plugin %s, the plugin condition does not match", + descriptor.getInformation().getName() + ) + ); + } + } + + private void verifyDependencies() { + Set dependencies = descriptor.getDependenciesWithVersion(); + for (NameAndVersion dependency : dependencies) { + NameAndVersion installed = context.find(dependency.getName()) + .orElseThrow( + () -> new DependencyNotFoundException(descriptor.getInformation().getName(), dependency.getName()) + ); + + dependency.getVersion().ifPresent(requiredVersion -> verifyDependencyVersion(dependency, installed)); + } + } + + private void verifyOptionalDependencies() { + Set dependencies = descriptor.getOptionalDependenciesWithVersion(); + for (NameAndVersion dependency : dependencies) { + Optional version = dependency.getVersion(); + if (version.isPresent()) { + Optional installed = context.find(dependency.getName()); + installed.ifPresent(nameAndVersion -> verifyDependencyVersion(dependency, nameAndVersion)); + } + } + } + + private void verifyDependencyVersion(NameAndVersion required, NameAndVersion installed) { + Version requiredVersion = required.mustGetVersion(); + Version installedVersion = installed.mustGetVersion(); + if (installedVersion.isOlder(requiredVersion)) { + throw new DependencyVersionMismatchException( + descriptor.getInformation().getName(), + required.getName(), + requiredVersion.getUnparsedVersion(), + installedVersion.getUnparsedVersion() + ); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java index 1ca34c5166..5c371aa323 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginInstaller.java @@ -38,36 +38,72 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Optional; -@SuppressWarnings("UnstableApiUsage") // guava hash is marked as unstable +@SuppressWarnings("UnstableApiUsage") + // guava hash is marked as unstable class PluginInstaller { - private final SCMContextProvider context; + private final SCMContextProvider scmContext; private final AdvancedHttpClient client; private final SmpDescriptorExtractor smpDescriptorExtractor; @Inject - public PluginInstaller(SCMContextProvider context, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) { - this.context = context; + public PluginInstaller(SCMContextProvider scmContext, AdvancedHttpClient client, SmpDescriptorExtractor smpDescriptorExtractor) { + this.scmContext = scmContext; this.client = client; this.smpDescriptorExtractor = smpDescriptorExtractor; } @SuppressWarnings("squid:S4790") // hashing should be safe - public PendingPluginInstallation install(AvailablePlugin plugin) { + public PendingPluginInstallation install(PluginInstallationContext context, AvailablePlugin plugin) { Path file = null; try (HashingInputStream input = new HashingInputStream(Hashing.sha256(), download(plugin))) { file = createFile(plugin); Files.copy(input, file); verifyChecksum(plugin, input.hash(), file); - verifyConditions(plugin, file); + + InstalledPluginDescriptor descriptor = smpDescriptorExtractor.extractPluginDescriptor(file); + verifyInformation(plugin.getDescriptor(), descriptor); + + PluginInstallationVerifier.verify(context, descriptor); + return new PendingPluginInstallation(plugin.install(), file); + } catch (PluginException ex) { + cleanup(file); + throw ex; } catch (IOException ex) { cleanup(file); throw new PluginDownloadException(plugin, ex); } } + private void verifyInformation(AvailablePluginDescriptor descriptorFromPluginCenter, InstalledPluginDescriptor downloadedDescriptor) { + verifyInformation(descriptorFromPluginCenter.getInformation(), downloadedDescriptor.getInformation()); + } + + private void verifyInformation(PluginInformation informationFromPluginCenter, PluginInformation downloadedInformation) { + if (!informationFromPluginCenter.getName().equals(downloadedInformation.getName())) { + throw new PluginInformationMismatchException( + informationFromPluginCenter, downloadedInformation, + String.format( + "downloaded plugin name \"%s\" does not match the expected name \"%s\" from plugin-center", + downloadedInformation.getName(), + informationFromPluginCenter.getName() + ) + ); + } + if (!informationFromPluginCenter.getVersion().equals(downloadedInformation.getVersion())) { + throw new PluginInformationMismatchException( + informationFromPluginCenter, downloadedInformation, + String.format( + "downloaded plugin version \"%s\" does not match the expected version \"%s\" from plugin-center", + downloadedInformation.getVersion(), + informationFromPluginCenter.getVersion() + ) + ); + } + } + private void cleanup(Path file) { try { if (file != null) { @@ -89,26 +125,12 @@ class PluginInstaller { } } - private void verifyConditions(AvailablePlugin plugin, Path file) throws IOException { - InstalledPluginDescriptor pluginDescriptor = smpDescriptorExtractor.extractPluginDescriptor(file); - if (!pluginDescriptor.getCondition().isSupported()) { - cleanup(file); - throw new PluginConditionFailedException( - pluginDescriptor.getCondition(), - String.format( - "could not load plugin %s, the plugin condition does not match", - plugin.getDescriptor().getInformation().getName() - ) - ); - } - } - private InputStream download(AvailablePlugin plugin) throws IOException { return client.get(plugin.getDescriptor().getUrl()).request().contentAsStream(); } private Path createFile(AvailablePlugin plugin) throws IOException { - Path directory = context.resolve(Paths.get("plugins")); + Path directory = scmContext.resolve(Paths.get("plugins")); Files.createDirectories(directory); return directory.resolve(plugin.getDescriptor().getInformation().getName() + ".smp"); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java index 8d9b137245..15e12bd6eb 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- @@ -35,7 +35,6 @@ import com.google.common.hash.Hashing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; -import sonia.scm.plugin.ExplodedSmp.PathTransformer; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @@ -50,11 +49,14 @@ import java.nio.file.Path; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Predicate; +import java.util.stream.Collectors; -import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; //~--- JDK imports ------------------------------------------------------------ @@ -103,6 +105,8 @@ public final class PluginProcessor //~--- constructors --------------------------------------------------------- + private final SmpDescriptorExtractor extractor = new SmpDescriptorExtractor(); + private ClassLoaderLifeCycle classLoaderLifeCycle; /** @@ -162,25 +166,16 @@ public final class PluginProcessor { logger.info("collect plugins"); - Set archives = collect(pluginDirectory, new PluginArchiveFilter()); + Set installedPlugins = findInstalledPlugins(); + logger.debug("found {} installed plugins", installedPlugins.size()); - logger.debug("extract {} archives", archives.size()); + Set newlyInstalledPlugins = installPending(installedPlugins); + logger.debug("finished installation of {} plugins", newlyInstalledPlugins.size()); - extract(archives); - - List dirs = - collectPluginDirectories(pluginDirectory) - .stream() - .filter(isPluginDirectory()) - .collect(toList()); - - logger.debug("process {} directories: {}", dirs.size(), dirs); - - List smps = Lists.transform(dirs, new PathTransformer()); + Set plugins = concat(installedPlugins, newlyInstalledPlugins); logger.trace("start building plugin tree"); - - PluginTree pluginTree = new PluginTree(smps); + PluginTree pluginTree = new PluginTree(plugins); logger.info("install plugin tree:\n{}", pluginTree); @@ -195,6 +190,52 @@ public final class PluginProcessor return ImmutableSet.copyOf(wrappers); } + private Set concat(Set installedPlugins, Set newlyInstalledPlugins) { + // We first add all newly installed plugins, + // after that we add the missing plugins, which are already installed. + // ExplodedSmp is equal by its path, so duplicates (updates) are not in the result. + return ImmutableSet.builder() + .addAll(newlyInstalledPlugins) + .addAll(installedPlugins) + .build(); + } + + private Set installPending(Set installedPlugins) throws IOException { + Set archives = collect(pluginDirectory, new PluginArchiveFilter()); + logger.debug("start installation of {} pending archives", archives.size()); + + Map pending = new HashMap<>(); + for (Path archive : archives) { + pending.put(archive, extractor.extractPluginDescriptor(archive)); + } + + PluginInstallationContext installationContext = PluginInstallationContext.fromDescriptors( + installedPlugins.stream().map(ExplodedSmp::getPlugin).collect(toSet()), + pending.values() + ); + + for (Map.Entry entry : pending.entrySet()) { + try { + PluginInstallationVerifier.verify(installationContext, entry.getValue()); + } catch (PluginException ex) { + Path path = entry.getKey(); + logger.error("failed to install smp {}, because it could not be verified", path); + logger.error("to restore scm-manager functionality remove the smp file {} from the plugin directory", path); + throw ex; + } + } + + return extract(archives); + } + + private Set findInstalledPlugins() throws IOException { + return collectPluginDirectories(pluginDirectory) + .stream() + .filter(isPluginDirectory()) + .map(ExplodedSmp::create) + .collect(Collectors.toSet()); + } + private Predicate isPluginDirectory() { return dir -> Files.exists(dir.resolve(DIRECTORY_METAINF).resolve("scm").resolve("plugin.xml")); } @@ -505,10 +546,12 @@ public final class PluginProcessor * * @throws IOException */ - private void extract(Iterable archives) throws IOException + private Set extract(Iterable archives) throws IOException { logger.debug("extract archives"); + ImmutableSet.Builder extracted = ImmutableSet.builder(); + for (Path archive : archives) { File archiveFile = archive.toFile(); @@ -519,17 +562,18 @@ public final class PluginProcessor logger.debug("extract plugin {}", smp.getPlugin()); - File directory = - PluginsInternal.createPluginDirectory(pluginDirectory.toFile(), - smp.getPlugin()); + File directory = PluginsInternal.createPluginDirectory(pluginDirectory.toFile(), smp.getPlugin()); - String checksum = com.google.common.io.Files.hash(archiveFile, - Hashing.sha256()).toString(); + String checksum = com.google.common.io.Files.hash(archiveFile, Hashing.sha256()).toString(); File checksumFile = PluginsInternal.getChecksumFile(directory); PluginsInternal.extract(smp, checksum, directory, checksumFile, false); moveArchive(archive); + + extracted.add(ExplodedSmp.create(directory.toPath())); } + + return extracted.build(); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java index fdcaf945d7..50a456eee1 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginTree.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Arrays; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; @@ -71,7 +72,7 @@ public final class PluginTree * * @param smps */ - public PluginTree(List smps) + public PluginTree(Collection smps) { smps.forEach(s -> { @@ -155,7 +156,8 @@ public final class PluginTree } private void append(StringBuilder buffer, String indent, PluginNode node) { - buffer.append(indent).append("+- ").append(node.getId()).append("\n"); + PluginInformation information = node.getPlugin().getPlugin().getInformation(); + buffer.append(indent).append("+- ").append(node.getId()).append("@").append(information.getVersion()).append("\n"); for (PluginNode child : node.getChildren()) { append(buffer, indent + " ", child); } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java b/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java index 80e78d6455..5e0e89aa35 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/SmpDescriptorExtractor.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; import javax.xml.bind.JAXBContext; @@ -36,7 +36,7 @@ import java.util.zip.ZipInputStream; class SmpDescriptorExtractor { InstalledPluginDescriptor extractPluginDescriptor(Path file) throws IOException { - try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8)) { + try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(file), StandardCharsets.UTF_8)) { ZipEntry nextEntry; while ((nextEntry = zipInputStream.getNextEntry()) != null) { if ("META-INF/scm/plugin.xml".equals(nextEntry.getName())) { diff --git a/scm-webapp/src/main/resources/locales/de/plugins.json b/scm-webapp/src/main/resources/locales/de/plugins.json index e5972f31c9..ebb15d7038 100644 --- a/scm-webapp/src/main/resources/locales/de/plugins.json +++ b/scm-webapp/src/main/resources/locales/de/plugins.json @@ -241,6 +241,18 @@ "displayName": "Fehler beim Löschen falscher Downloads", "description": "Ein fehlerhaft heruntergeladenes Plugin konnte nicht gelöscht werden. Bitte prüfen Sie die Server Logs und löschen die Datei manuell." }, + "5GS6lwvWF1": { + "displayName": "Abhänigkeit konnte nicht gefunden werden", + "description": "Eine der Abhänigkeiten des Plugins konnte nicht gefunden werden. Bitte prüfen Sie die Logs für weitere Informationen." + }, + "E5S6niWwi1": { + "displayName": "Version einer Abhänigkeit zu niedrig", + "description": "Die Version einer Abhänigkeit des Plugin ist zu niedrig. Bitte prüfen Sie die Logs für weitere Informationen." + }, + "4RS6niPRX1": { + "displayName": "Plugin information stimmen nicht überein", + "description": "Die Informationen des heruntergeladenen Plugins stimmen nicht mit den Informationen des Plugin Centers überein. Bitte prüfen Sie die Logs für weitere Informationen." + }, "2qRyyaVcJ1": { "displayName": "Ungültig formatiertes Element", "description": "Die Eingabe beinhaltete unfültige Formate. Bitte prüfen Sie die Server Logs für genauere Informationen." diff --git a/scm-webapp/src/main/resources/locales/en/plugins.json b/scm-webapp/src/main/resources/locales/en/plugins.json index 8a897f82ff..5cb6ae09df 100644 --- a/scm-webapp/src/main/resources/locales/en/plugins.json +++ b/scm-webapp/src/main/resources/locales/en/plugins.json @@ -241,6 +241,18 @@ "displayName": "Error while cleaning up failed plugin", "description": "A failed plugin download could not be removed correctly. Please check the server log and remove the plugin manually." }, + "5GS6lwvWF1": { + "displayName": "Dependency not found", + "description": "One of the plugin dependencies could not be found. Please check the server logs for more details." + }, + "E5S6niWwi1": { + "displayName": "Dependency version mismatch", + "description": "The plugin depends on a newer version of an already installed plugin. Please check the server logs for more details." + }, + "4RS6niPRX1": { + "displayName": "Plugin information mismatch", + "description": "The downloaded plugin does not match the information provided by the plugin center. Please check the server logs for more details." + }, "2qRyyaVcJ1": { "displayName": "Invalid format in element", "description": "The input had some invalid formats. Please check the server log for further information." diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index b16d010c5d..977451ca3a 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -90,20 +90,28 @@ class DefaultPluginManagerTest { @Captor private ArgumentCaptor eventCaptor; - @InjectMocks private DefaultPluginManager manager; @Mock private Subject subject; + private final PluginInstallationContext context = PluginInstallationContext.empty(); + @BeforeEach void mockInstaller() { - lenient().when(installer.install(any())).then(ic -> { - AvailablePlugin plugin = ic.getArgument(0); + lenient().when(installer.install(any(), any())).then(ic -> { + AvailablePlugin plugin = ic.getArgument(1); return new PendingPluginInstallation(plugin.install(), null); }); } + @BeforeEach + void setUpObjectUnderTest() { + manager = new DefaultPluginManager( + loader, center, installer, restarter, eventBus, plugins -> context + ); + } + @Nested class WithAdminPermissions { @@ -209,7 +217,7 @@ class DefaultPluginManagerTest { manager.install("scm-git-plugin", false); - verify(installer).install(git); + verify(installer).install(context, git); verify(restarter, never()).restart(any(), any()); } @@ -222,8 +230,8 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); - verify(installer).install(mail); - verify(installer).install(review); + verify(installer).install(context, mail); + verify(installer).install(context, review); } @Test @@ -239,7 +247,7 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); ArgumentCaptor captor = ArgumentCaptor.forClass(AvailablePlugin.class); - verify(installer).install(captor.capture()); + verify(installer).install(any(), captor.capture()); assertThat(captor.getValue().getDescriptor().getInformation().getName()).isEqualTo("scm-review-plugin"); } @@ -256,8 +264,8 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); - verify(installer).install(mail); - verify(installer).install(review); + verify(installer).install(context, mail); + verify(installer).install(context, review); } @Test @@ -272,8 +280,8 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); - verify(installer).install(mail); - verify(installer).install(review); + verify(installer).install(context, mail); + verify(installer).install(context, review); } @Test @@ -285,8 +293,8 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); - verify(installer, never()).install(mail); - verify(installer).install(review); + verify(installer, never()).install(context, mail); + verify(installer).install(context, review); } @Test @@ -299,12 +307,12 @@ class DefaultPluginManagerTest { when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification)); PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class); - doReturn(pendingNotification).when(installer).install(notification); + doReturn(pendingNotification).when(installer).install(context, notification); PendingPluginInstallation pendingMail = mock(PendingPluginInstallation.class); - doReturn(pendingMail).when(installer).install(mail); + doReturn(pendingMail).when(installer).install(context, mail); - doThrow(new PluginChecksumMismatchException(mail, "1", "2")).when(installer).install(review); + doThrow(new PluginChecksumMismatchException(mail, "1", "2")).when(installer).install(context, review); assertThrows(PluginInstallException.class, () -> manager.install("scm-review-plugin", false)); @@ -322,7 +330,7 @@ class DefaultPluginManagerTest { assertThrows(NotFoundException.class, () -> manager.install("scm-review-plugin", false)); - verify(installer, never()).install(any()); + verify(installer, never()).install(any(), any()); } @Test @@ -332,7 +340,7 @@ class DefaultPluginManagerTest { manager.install("scm-git-plugin", true); - verify(installer).install(git); + verify(installer).install(context, git); verify(restarter).restart(any(), any()); } @@ -353,7 +361,7 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); manager.install("scm-review-plugin", false); // only one interaction - verify(installer).install(any()); + verify(installer).install(any(), any()); } @Test @@ -538,7 +546,7 @@ class DefaultPluginManagerTest { AvailablePlugin git = createAvailable("scm-git-plugin"); when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); PendingPluginInstallation gitPendingPluginInformation = mock(PendingPluginInstallation.class); - when(installer.install(git)).thenReturn(gitPendingPluginInformation); + when(installer.install(context, git)).thenReturn(gitPendingPluginInformation); manager.install("scm-git-plugin", false); manager.uninstall("scm-ssh-plugin", false); @@ -571,8 +579,8 @@ class DefaultPluginManagerTest { manager.updateAll(); - verify(installer).install(newMailPlugin); - verify(installer).install(newReviewPlugin); + verify(installer).install(context, newMailPlugin); + verify(installer).install(context, newReviewPlugin); } @@ -587,7 +595,7 @@ class DefaultPluginManagerTest { manager.updateAll(); - verify(installer, never()).install(oldScriptPlugin); + verify(installer, never()).install(context, oldScriptPlugin); } @Test @@ -607,7 +615,7 @@ class DefaultPluginManagerTest { void shouldFirePluginEventOnFailedInstallation() { AvailablePlugin review = createAvailable("scm-review-plugin"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); - doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(review); + doThrow(new PluginDownloadException(review, new IOException())).when(installer).install(context, review); assertThrows(PluginDownloadException.class, () -> manager.install("scm-review-plugin", false)); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationContextTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationContextTest.java new file mode 100644 index 0000000000..621e705c04 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationContextTest.java @@ -0,0 +1,115 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginInstallationContextTest { + + @Test + void shouldReturnInstalledPlugin() { + Set installed = installed("scm-git-plugin", "1.0.0"); + Set pending = Collections.emptySet(); + + PluginInstallationContext context = PluginInstallationContext.from(installed, pending); + Optional plugin = context.find("scm-git-plugin"); + assertThat(plugin).contains(new NameAndVersion("scm-git-plugin", "1.0.0")); + } + + @Test + void shouldReturnPendingPlugin() { + Set installed = Collections.emptySet(); + Set pending = pending("scm-hg-plugin", "1.0.0"); + + PluginInstallationContext context = PluginInstallationContext.from(installed, pending); + Optional plugin = context.find("scm-hg-plugin"); + assertThat(plugin).contains(new NameAndVersion("scm-hg-plugin", "1.0.0")); + } + + @Test + void shouldReturnPendingEvenWithInstalled() { + Set installed = installed("scm-svn-plugin", "1.1.0"); + Set pending = pending("scm-svn-plugin", "1.2.0"); + + PluginInstallationContext context = PluginInstallationContext.from(installed, pending); + Optional plugin = context.find("scm-svn-plugin"); + assertThat(plugin).contains(new NameAndVersion("scm-svn-plugin", "1.2.0")); + } + + @Test + void shouldReturnEmpty() { + Set installed = Collections.emptySet(); + Set pending = Collections.emptySet(); + + PluginInstallationContext context = PluginInstallationContext.from(installed, pending); + Optional plugin = context.find("scm-legacy-plugin"); + assertThat(plugin).isEmpty(); + } + + @Test + void shouldCreateContextFromDescriptor() { + Set installed = mockDescriptor(InstalledPluginDescriptor.class, "scm-svn-plugin", "1.1.0"); + Set pending = mockDescriptor(AvailablePluginDescriptor.class, "scm-svn-plugin", "1.2.0"); + + PluginInstallationContext context = PluginInstallationContext.fromDescriptors(installed, pending); + Optional plugin = context.find("scm-svn-plugin"); + assertThat(plugin).contains(new NameAndVersion("scm-svn-plugin", "1.2.0")); + } + + private Set installed(String name, String version) { + return mockPlugin(InstalledPlugin.class, name, version); + } + + private Set pending(String name, String version) { + return mockPlugin(AvailablePlugin.class, name, version); + } + + private

Set

mockPlugin(Class

pluginClass, String name, String version) { + P plugin = mock(pluginClass, Answers.RETURNS_DEEP_STUBS); + when(plugin.getDescriptor().getInformation().getName()).thenReturn(name); + when(plugin.getDescriptor().getInformation().getVersion()).thenReturn(version); + return Collections.singleton(plugin); + } + + private Set mockDescriptor(Class descriptorClass, String name, String version) { + D desc = mock(descriptorClass, Answers.RETURNS_DEEP_STUBS); + when(desc.getInformation().getName()).thenReturn(name); + when(desc.getInformation().getVersion()).thenReturn(version); + return Collections.singleton(desc); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationVerifierTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationVerifierTest.java new file mode 100644 index 0000000000..37597c806f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallationVerifierTest.java @@ -0,0 +1,174 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.Collections; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static sonia.scm.plugin.PluginInstallationContext.empty; + +@ExtendWith(MockitoExtension.class) +class PluginInstallationVerifierTest { + + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private InstalledPluginDescriptor descriptor; + + // hog stands for "Heart of Gold" + private static final String HOG_PLUGIN = "scm-hog-plugin"; + + // iid stands for "Infinite Improbability Drive" + private static final String IID_PLUGIN = "scm-iid-plugin"; + + @BeforeEach + void setUpDescriptor() { + PluginInformation information = new PluginInformation(); + information.setName(HOG_PLUGIN); + information.setVersion("1.0.0"); + when(descriptor.getInformation()).thenReturn(information); + } + + @Test + void shouldFailOnCondition() { + PluginInstallationContext context = empty(); + assertThrows(PluginConditionFailedException.class, () -> PluginInstallationVerifier.verify(context, descriptor)); + } + + @Test + void shouldFailOnMissingDependency() { + matchConditions(); + when(descriptor.getDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(IID_PLUGIN))); + PluginInstallationContext context = empty(); + + DependencyNotFoundException exception = assertThrows( + DependencyNotFoundException.class, () -> PluginInstallationVerifier.verify(context, descriptor) + ); + assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN); + assertThat(exception.getMissingDependency()).isEqualTo(IID_PLUGIN); + } + + private void matchConditions() { + when(descriptor.getCondition().isSupported()).thenReturn(true); + } + + @Test + void shouldFailOnDependencyVersionMismatch() { + matchConditions(); + + // mock installation of iid 1.0.0 + PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.0.0"); + + // mock dependency of iid 1.1.0 + mockDependingOf(IID_PLUGIN, "1.1.0"); + + DependencyVersionMismatchException exception = assertThrows( + DependencyVersionMismatchException.class, () -> PluginInstallationVerifier.verify(context, descriptor) + ); + assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN); + assertThat(exception.getDependency()).isEqualTo(IID_PLUGIN); + assertThat(exception.getMinVersion()).isEqualTo("1.1.0"); + assertThat(exception.getCurrentVersion()).isEqualTo("1.0.0"); + } + + @Test + void shouldFailOnOptionalDependencyVersionMismatch() { + matchConditions(); + + // mock installation of iid 1.0.0 + PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.0.0"); + + // mock dependency of iid 1.1.0 + mockOptionalDependingOf(IID_PLUGIN, "1.1.0"); + + DependencyVersionMismatchException exception = assertThrows( + DependencyVersionMismatchException.class, () -> PluginInstallationVerifier.verify(context, descriptor) + ); + assertThat(exception.getPlugin()).isEqualTo(HOG_PLUGIN); + assertThat(exception.getDependency()).isEqualTo(IID_PLUGIN); + assertThat(exception.getMinVersion()).isEqualTo("1.1.0"); + assertThat(exception.getCurrentVersion()).isEqualTo("1.0.0"); + } + + @Test + @SuppressWarnings("squid:S2699") // we are happy if no exception is thrown + void shouldVerifyPlugin() { + matchConditions(); + + PluginInstallationContext context = empty(); + PluginInstallationVerifier.verify(context, descriptor); + } + + @Test + @SuppressWarnings("squid:S2699") // we are happy if no exception is thrown + void shouldVerifyPluginWithDependencies() { + matchConditions(); + + // mock installation of iid 1.1.0 + PluginInstallationContext context = mockInstallationOf(IID_PLUGIN, "1.1.0"); + + // mock dependency of iid 1.1.0 + mockDependingOf(IID_PLUGIN, "1.1.0"); + + PluginInstallationVerifier.verify(context, descriptor); + } + + @Test + @SuppressWarnings("squid:S2699") // we are happy if no exception is thrown + void shouldVerifyPluginWithOptionalDependency() { + matchConditions(); + + PluginInstallationContext context = PluginInstallationContext.empty(); + + // mock dependency of iid 1.1.0 + mockOptionalDependingOf(IID_PLUGIN, "1.1.0"); + + PluginInstallationVerifier.verify(context, descriptor); + } + + private void mockOptionalDependingOf(String plugin, String version) { + when(descriptor.getOptionalDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(plugin, version))); + } + + private void mockDependingOf(String plugin, String version) { + when(descriptor.getDependenciesWithVersion()).thenReturn(Collections.singleton(new NameAndVersion(plugin, version))); + } + + private PluginInstallationContext mockInstallationOf(String plugin, String version) { + PluginInstallationContext context = mock(PluginInstallationContext.class); + when(context.find(IID_PLUGIN)).thenReturn(Optional.of(new NameAndVersion(plugin, version))); + return context; + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java index bccbb67f3c..a619289094 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginInstallerTest.java @@ -83,7 +83,7 @@ class PluginInstallerTest { void shouldDownloadPlugin() throws IOException { mockContent("42"); - installer.install(createGitPlugin()); + installer.install(PluginInstallationContext.empty(), createGitPlugin()); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).hasContent("42"); } @@ -93,7 +93,7 @@ class PluginInstallerTest { mockContent("42"); AvailablePlugin gitPlugin = createGitPlugin(); - PendingPluginInstallation pending = installer.install(gitPlugin); + PendingPluginInstallation pending = installer.install(PluginInstallationContext.empty(), gitPlugin); assertThat(pending).isNotNull(); assertThat(pending.getPlugin().getDescriptor()).isEqualTo(gitPlugin.getDescriptor()); @@ -117,14 +117,18 @@ class PluginInstallerTest { void shouldThrowPluginDownloadException() throws IOException { when(client.get("https://download.hitchhiker.com").request()).thenThrow(new IOException("failed to download")); - assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + assertThrows(PluginDownloadException.class, () -> installer.install(context, gitPlugin)); } @Test void shouldThrowPluginChecksumMismatchException() throws IOException { mockContent("21"); - assertThrows(PluginChecksumMismatchException.class, () -> installer.install(createGitPlugin())); + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + assertThrows(PluginChecksumMismatchException.class, () -> installer.install(context, gitPlugin)); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); } @@ -134,7 +138,9 @@ class PluginInstallerTest { when(stream.read(any(), anyInt(), anyInt())).thenThrow(new IOException("failed to read")); when(client.get("https://download.hitchhiker.com").request().contentAsStream()).thenReturn(stream); - assertThrows(PluginDownloadException.class, () -> installer.install(createGitPlugin())); + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + assertThrows(PluginDownloadException.class, () -> installer.install(context, gitPlugin)); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); } @@ -144,13 +150,44 @@ class PluginInstallerTest { InstalledPluginDescriptor supportedPlugin = createPluginDescriptor(false); when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); - assertThrows(PluginConditionFailedException.class, () -> installer.install(createGitPlugin())); + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + assertThrows(PluginConditionFailedException.class, () -> installer.install(context, gitPlugin)); assertThat(directory.resolve("plugins").resolve("scm-git-plugin.smp")).doesNotExist(); } + @Test + void shouldFailForNameMismatch() throws IOException { + mockContent("42"); + + InstalledPluginDescriptor supportedPlugin = createPluginDescriptor("scm-svn-plugin", "1.0.0", true); + when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); + + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + PluginInformationMismatchException exception = assertThrows(PluginInformationMismatchException.class, () -> installer.install(context, gitPlugin)); + assertThat(exception.getApi().getName()).isEqualTo("scm-git-plugin"); + assertThat(exception.getDownloaded().getName()).isEqualTo("scm-svn-plugin"); + } + + @Test + void shouldFailForVersionMismatch() throws IOException { + mockContent("42"); + + InstalledPluginDescriptor supportedPlugin = createPluginDescriptor("scm-git-plugin", "1.1.0", true); + when(extractor.extractPluginDescriptor(any())).thenReturn(supportedPlugin); + + PluginInstallationContext context = PluginInstallationContext.empty(); + AvailablePlugin gitPlugin = createGitPlugin(); + PluginInformationMismatchException exception = assertThrows(PluginInformationMismatchException.class, () -> installer.install(context, gitPlugin)); + assertThat(exception.getApi().getVersion()).isEqualTo("1.0.0"); + assertThat(exception.getDownloaded().getVersion()).isEqualTo("1.1.0"); + } + private AvailablePlugin createPlugin(String name, String url, String checksum) { PluginInformation information = new PluginInformation(); information.setName(name); + information.setVersion("1.0.0"); AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( information, null, Collections.emptySet(), url, checksum ); @@ -158,9 +195,15 @@ class PluginInstallerTest { } private InstalledPluginDescriptor createPluginDescriptor(boolean supported) { + return createPluginDescriptor("scm-git-plugin", "1.0.0", supported); + } + + private InstalledPluginDescriptor createPluginDescriptor(String name, String version, boolean supported) { InstalledPluginDescriptor installedPluginDescriptor = mock(InstalledPluginDescriptor.class, RETURNS_DEEP_STUBS); + lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn(name); + lenient().when(installedPluginDescriptor.getInformation().getName()).thenReturn(name); + lenient().when(installedPluginDescriptor.getInformation().getVersion()).thenReturn(version); lenient().when(installedPluginDescriptor.getCondition().isSupported()).thenReturn(supported); - lenient().when(installedPluginDescriptor.getInformation().getId()).thenReturn("scm-git-plugin"); return installedPluginDescriptor; } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java index 923e569d16..f27aa19982 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginProcessorTest.java @@ -21,164 +21,182 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.plugin; //~--- non-JDK imports -------------------------------------------------------- import com.google.common.base.Charsets; -import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.io.Resources; - -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import sonia.scm.lifecycle.classloading.ClassLoaderLifeCycle; -import static org.hamcrest.Matchers.*; - -import static org.junit.Assert.*; - -//~--- JDK imports ------------------------------------------------------------ - +import javax.xml.bind.JAXB; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; - -import java.lang.reflect.InvocationTargetException; - import java.net.URL; - +import java.nio.file.Path; import java.util.Set; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; /** * * @author Sebastian Sdorra */ -public class PluginProcessorTest -{ +class PluginProcessorTest { - /** Field description */ private static final PluginResource PLUGIN_A = new PluginResource("sonia/scm/plugin/scm-a-plugin.smp", "scm-a-plugin.smp", "scm-a-plugin:1.0.0-SNAPSHOT"); - /** Field description */ private static final PluginResource PLUGIN_B = new PluginResource("sonia/scm/plugin/scm-b-plugin.smp", "scm-b-plugin.smp", "scm-b-plugin:1.0.0-SNAPSHOT"); - /** Field description */ private static final PluginResource PLUGIN_C = new PluginResource("sonia/scm/plugin/scm-c-plugin.smp", "scm-c-plugin.smp", "scm-c-plugin:1.0.0-SNAPSHOT"); - /** Field description */ private static final PluginResource PLUGIN_D = new PluginResource("sonia/scm/plugin/scm-d-plugin.smp", "scm-d-plugin.smp", "scm-d-plugin:1.0.0-SNAPSHOT"); - /** Field description */ private static final PluginResource PLUGIN_E = new PluginResource("sonia/scm/plugin/scm-e-plugin.smp", "scm-e-plugin.smp", "scm-e-plugin:1.0.0-SNAPSHOT"); - /** Field description */ private static final PluginResource PLUGIN_F_1_0_0 = new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.0.smp", "scm-f-plugin.smp", "scm-f-plugin:1.0.0"); - /** Field description */ private static final PluginResource PLUGIN_F_1_0_1 = new PluginResource("sonia/scm/plugin/scm-f-plugin-1.0.1.smp", "scm-f-plugin.smp", "scm-f-plugin:1.0.1"); - //~--- methods -------------------------------------------------------------- + private static final String PLUGIN_G = "scm-g-plugin"; + private static final String PLUGIN_H = "scm-h-plugin"; + private static final String PLUGIN_I = "scm-i-plugin"; - /** - * Method description - * - * - * @throws IOException - */ - @Test(expected = PluginCircularDependencyException.class) - public void testCircularDependencies() throws IOException - { + private File pluginDirectory; + private PluginProcessor processor; + + @BeforeEach + void setUp(@TempDir Path tempDirectoryPath) { + pluginDirectory = tempDirectoryPath.toFile(); + processor = new PluginProcessor(ClassLoaderLifeCycle.create(), tempDirectoryPath); + } + + + @Test + void shouldFailOnPluginCondition() throws IOException { + createPendingPluginInstallation(PLUGIN_G); + + assertThrows(PluginConditionFailedException.class, this::collectPlugins); + } + + + @Test + void shouldFailOnWrongDependencyVersion() throws IOException { + createPendingPluginInstallation(PLUGIN_H); + createPendingPluginInstallation(PLUGIN_I); + assertThrows(DependencyVersionMismatchException.class, this::collectPlugins); + } + + @Test + void shouldNotContainDuplicatesOnUpdate() throws IOException { + createInstalledPlugin("scm-mail-plugin-2-0-0"); + createInstalledPlugin("scm-review-plugin-2-0-0"); + createPendingPluginInstallation("scm-mail-plugin-2-1-0"); + createPendingPluginInstallation("scm-review-plugin-2-1-0"); + + Set plugins = collectPlugins().stream() + .map(p -> p.getDescriptor().getInformation().getName(true)) + .collect(Collectors.toSet()); + assertThat(plugins).containsOnly("scm-mail-plugin:2.1.0", "scm-review-plugin:2.1.0"); + } + + @SuppressWarnings("UnstableApiUsage") + private void createPendingPluginInstallation(String descriptorResource) throws IOException { + URL resource = resource(descriptorResource); + InstalledPluginDescriptor descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class); + + File file = new File(pluginDirectory, descriptor.getInformation().getName() + ".smp"); + + try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(file))) { + zip.putNextEntry(new ZipEntry("META-INF/scm/plugin.xml")); + Resources.copy(resource, zip); + } + } + + @SuppressWarnings("UnstableApiUsage") + private void createInstalledPlugin(String descriptorResource) throws IOException { + URL resource = resource(descriptorResource); + InstalledPluginDescriptor descriptor = JAXB.unmarshal(resource, InstalledPluginDescriptor.class); + + File directory = new File(pluginDirectory, descriptor.getInformation().getName()); + File scmDirectory = new File(directory, "META-INF" + File.separator + "scm"); + assertThat(scmDirectory.mkdirs()).isTrue(); + + try (OutputStream output = new FileOutputStream(new File(scmDirectory, "plugin.xml"))) { + Resources.copy(resource, output); + } + } + + @SuppressWarnings("UnstableApiUsage") + private URL resource(String descriptorResource) { + return Resources.getResource("sonia/scm/plugin/" + descriptorResource + ".xml"); + } + + @Test + void shouldFailOnCircularDependencies() throws IOException { copySmps(PLUGIN_C, PLUGIN_D, PLUGIN_E); - collectPlugins(); + assertThrows(PluginCircularDependencyException.class, this::collectPlugins); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testCollectPlugins() throws IOException - { + void shouldCollectPlugins() throws IOException { copySmp(PLUGIN_A); InstalledPlugin plugin = collectAndGetFirst(); - - assertThat(plugin.getId(), is(PLUGIN_A.id)); + assertThat(plugin.getId()).isEqualTo(PLUGIN_A.id); } @Test - public void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException { - new File(pluginDirectory, "some-directory").mkdirs(); + void shouldCollectPluginsAndDoNotFailOnNonPluginDirectories() throws IOException { + assertThat(new File(pluginDirectory, "some-directory").mkdirs()).isTrue(); copySmp(PLUGIN_A); InstalledPlugin plugin = collectAndGetFirst(); - assertThat(plugin.getId(), is(PLUGIN_A.id)); + assertThat(plugin.getId()).isEqualTo(PLUGIN_A.id); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testCollectPluginsWithDependencies() throws IOException - { + void shouldCollectPluginsWithDependencies() throws IOException { copySmps(PLUGIN_A, PLUGIN_B); Set plugins = collectPlugins(); - - assertThat(plugins, hasSize(2)); + assertThat(plugins).hasSize(2); InstalledPlugin a = findPlugin(plugins, PLUGIN_A.id); - - assertNotNull(a); + assertThat(a).isNotNull(); InstalledPlugin b = findPlugin(plugins, PLUGIN_B.id); - - assertNotNull(b); + assertThat(b).isNotNull(); } - /** - * Method description - * - * - * @throws ClassNotFoundException - * @throws IOException - * @throws IllegalAccessException - * @throws IllegalArgumentException - * @throws InstantiationException - * @throws InvocationTargetException - * @throws NoSuchMethodException - */ @Test - public void testPluginClassLoader() - throws IOException, ClassNotFoundException, InstantiationException, - IllegalAccessException, NoSuchMethodException, IllegalArgumentException, - InvocationTargetException - { + void shouldCreateWorkingPluginClassLoader() throws Exception { copySmp(PLUGIN_A); InstalledPlugin plugin = collectAndGetFirst(); @@ -187,36 +205,20 @@ public class PluginProcessorTest // load parent class Class clazz = cl.loadClass(PluginResource.class.getName()); - assertSame(PluginResource.class, clazz); + assertThat(PluginResource.class).isSameAs(clazz); // load packaged class clazz = cl.loadClass("sonia.scm.plugins.HelloService"); - assertNotNull(clazz); + assertThat(clazz).isNotNull(); Object instance = clazz.newInstance(); Object result = clazz.getMethod("sayHello").invoke(instance); - assertEquals("hello", result); + assertThat(result).isEqualTo("hello"); } - /** - * Method description - * - * - * @throws ClassNotFoundException - * @throws IOException - * @throws IllegalAccessException - * @throws IllegalArgumentException - * @throws InstantiationException - * @throws InvocationTargetException - * @throws NoSuchMethodException - */ @Test - public void testPluginClassLoaderWithDependencies() - throws IOException, ClassNotFoundException, InstantiationException, - IllegalAccessException, NoSuchMethodException, IllegalArgumentException, - InvocationTargetException - { + void shouldCreateWorkingPluginClassLoaderWithDependencies() throws Exception { copySmps(PLUGIN_A, PLUGIN_B); Set plugins = collectPlugins(); @@ -227,213 +229,88 @@ public class PluginProcessorTest // load parent class Class clazz = cl.loadClass(PluginResource.class.getName()); - assertSame(PluginResource.class, clazz); + assertThat(PluginResource.class).isSameAs(clazz); // load packaged class clazz = cl.loadClass("sonia.scm.plugins.HelloAgainService"); - assertNotNull(clazz); + assertThat(clazz).isNotNull(); Object instance = clazz.newInstance(); Object result = clazz.getMethod("sayHelloAgain").invoke(instance); - assertEquals("hello again", result); + assertThat(result).isEqualTo("hello again"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testPluginWebResourceLoader() throws IOException - { + @SuppressWarnings("UnstableApiUsage") + void shouldCreatePluginWebResourceLoader() throws IOException { copySmp(PLUGIN_A); InstalledPlugin plugin = collectAndGetFirst(); WebResourceLoader wrl = plugin.getWebResourceLoader(); - - assertNotNull(wrl); + assertThat(wrl).isNotNull(); URL url = wrl.getResource("hello"); + assertThat(url).isNotNull(); - assertNotNull(url); - assertThat(Resources.toString(url, Charsets.UTF_8), is("hello")); + assertThat(Resources.toString(url, Charsets.UTF_8)).isEqualTo("hello"); } - /** - * Method description - * - * - * @throws IOException - */ @Test - public void testUpdate() throws IOException - { + void shouldDoPluginUpdate() throws IOException { copySmp(PLUGIN_F_1_0_0); - InstalledPlugin plugin = collectAndGetFirst(); + assertThat(plugin.getId()).isEqualTo(PLUGIN_F_1_0_0.id); - assertThat(plugin.getId(), is(PLUGIN_F_1_0_0.id)); copySmp(PLUGIN_F_1_0_1); plugin = collectAndGetFirst(); - assertThat(plugin.getId(), is(PLUGIN_F_1_0_1.id)); + assertThat(plugin.getId()).isEqualTo(PLUGIN_F_1_0_1.id); } - //~--- set methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @throws IOException - */ - @Before - public void setUp() throws IOException - { - pluginDirectory = temp.newFolder(); - processor = new PluginProcessor(ClassLoaderLifeCycle.create(), pluginDirectory.toPath()); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @return - * - * @throws IOException - */ - private InstalledPlugin collectAndGetFirst() throws IOException - { + private InstalledPlugin collectAndGetFirst() throws IOException { Set plugins = collectPlugins(); - assertThat(plugins, hasSize(1)); + assertThat(plugins).hasSize(1); return Iterables.get(plugins, 0); } - /** - * Method description - * - * - * @return - * - * @throws IOException - */ - private Set collectPlugins() throws IOException - { + private Set collectPlugins() throws IOException { return processor.collectPlugins(PluginProcessorTest.class.getClassLoader()); } - /** - * Method description - * - * - * @param plugin - * - * @throws IOException - */ - private void copySmp(PluginResource plugin) throws IOException - { + @SuppressWarnings("UnstableApiUsage") + private void copySmp(PluginResource plugin) throws IOException { URL resource = Resources.getResource(plugin.path); File file = new File(pluginDirectory, plugin.name); - try (OutputStream out = new FileOutputStream(file)) - { + try (OutputStream out = new FileOutputStream(file)) { Resources.copy(resource, out); } } - /** - * Method description - * - * - * @param plugins - * - * @throws IOException - */ - private void copySmps(PluginResource... plugins) throws IOException - { - for (PluginResource plugin : plugins) - { + private void copySmps(PluginResource... plugins) throws IOException { + for (PluginResource plugin : plugins) { copySmp(plugin); } } - /** - * Method description - * - * - * @param plugin - * @param id - * - * @return - */ - private InstalledPlugin findPlugin(Iterable plugin, - final String id) - { - return Iterables.find(plugin, new Predicate() - { - - @Override - public boolean apply(InstalledPlugin input) - { - return id.equals(input.getId()); - } - }); + private InstalledPlugin findPlugin(Iterable plugin, final String id) { + return Iterables.find(plugin, input -> id.equals(input.getId())); } + private static class PluginResource { - //~--- inner classes -------------------------------------------------------- + private final String path; + private final String name; + private final String id; - /** - * Class description - * - * - * @version Enter version here..., 14/12/06 - * @author Enter your name here... - */ - private static class PluginResource - { - /** - * Constructs ... - * - * - * @param path - * @param name - * @param id - */ - public PluginResource(String path, String name, String id) - { + public PluginResource(String path, String name, String id) { this.path = path; this.name = name; this.id = id; } - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private final String id; - - /** Field description */ - private final String name; - - /** Field description */ - private final String path; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - @Rule - public TemporaryFolder temp = new TemporaryFolder(); - - /** Field description */ - private File pluginDirectory; - - /** Field description */ - private PluginProcessor processor; } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java index dd8dad5eca..79f5d29060 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/SmpDescriptorExtractorTest.java @@ -63,11 +63,13 @@ class SmpDescriptorExtractorTest { "\n" + "\n"; + private final SmpDescriptorExtractor extractor = new SmpDescriptorExtractor(); + @Test void shouldExtractPluginXml(@TempDir Path tempDir) throws IOException { Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", PLUGIN_XML); - InstalledPluginDescriptor installedPluginDescriptor = new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile); + InstalledPluginDescriptor installedPluginDescriptor = extractor.extractPluginDescriptor(pluginFile); Assertions.assertThat(installedPluginDescriptor.getInformation().getName()).isEqualTo("scm-test-plugin"); } @@ -76,14 +78,14 @@ class SmpDescriptorExtractorTest { void shouldFailWithoutPluginXml(@TempDir Path tempDir) throws IOException { Path pluginFile = createZipFile(tempDir, "META-INF/wrong/plugin.xml", PLUGIN_XML); - assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); + assertThrows(IOException.class, () -> extractor.extractPluginDescriptor(pluginFile)); } @Test void shouldFailWithIllegalPluginXml(@TempDir Path tempDir) throws IOException { Path pluginFile = createZipFile(tempDir, "META-INF/scm/plugin.xml", "content"); - assertThrows(IOException.class, () -> new SmpDescriptorExtractor().extractPluginDescriptor(pluginFile)); + assertThrows(IOException.class, () -> extractor.extractPluginDescriptor(pluginFile)); } Path createZipFile(Path tempDir, String internalFileName, String content) throws IOException { diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-g-plugin.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-g-plugin.xml new file mode 100644 index 0000000000..8fca8bbdfc --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-g-plugin.xml @@ -0,0 +1,47 @@ + + + + + 2 + + + sonia.scm.plugins + scm-g-plugin + 1.0.0 + scm-g-plugin + Plugin g has a min version over 9000 + + + + 9000.0.0 + + + + sonia.scm.plugins + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-h-plugin.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-h-plugin.xml new file mode 100644 index 0000000000..fb7977da6f --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-h-plugin.xml @@ -0,0 +1,43 @@ + + + + + 2 + + + sonia.scm.plugins + scm-h-plugin + 1.0.0 + scm-h-plugin + Plugin h is nothing special + + + + sonia.scm.plugins + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-i-plugin.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-i-plugin.xml new file mode 100644 index 0000000000..e49dd45da8 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-i-plugin.xml @@ -0,0 +1,47 @@ + + + + + 2 + + + sonia.scm.plugins + scm-i-plugin + 1.0.0 + scm-i-plugin + Plugin i depends on h in version 1.2.0 + + + + sonia.scm.plugins + + + + scm-h-plugin + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-0-0.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-0-0.xml new file mode 100644 index 0000000000..bf148adaac --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-0-0.xml @@ -0,0 +1,43 @@ + + + + + 2 + + + sonia.scm.plugins + scm-mail-plugin + 2.0.0 + scm-mail-plugin + The awesome mail plugin + + + + sonia.scm.plugins + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-1-0.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-1-0.xml new file mode 100644 index 0000000000..f642b1e65d --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-mail-plugin-2-1-0.xml @@ -0,0 +1,43 @@ + + + + + 2 + + + sonia.scm.plugins + scm-mail-plugin + 2.1.0 + scm-mail-plugin + The awesome mail plugin + + + + sonia.scm.plugins + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-0-0.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-0-0.xml new file mode 100644 index 0000000000..43517a4f47 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-0-0.xml @@ -0,0 +1,47 @@ + + + + + 2 + + + sonia.scm.plugins + scm-review-plugin + 2.0.0 + scm-review-plugin + The awesome review plugin + + + + sonia.scm.plugins + + + + scm-mail-plugin + + + diff --git a/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-1-0.xml b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-1-0.xml new file mode 100644 index 0000000000..4feab0b7c4 --- /dev/null +++ b/scm-webapp/src/test/resources/sonia/scm/plugin/scm-review-plugin-2-1-0.xml @@ -0,0 +1,47 @@ + + + + + 2 + + + sonia.scm.plugins + scm-review-plugin + 2.1.0 + scm-review-plugin + The awesome review plugin + + + + sonia.scm.plugins + + + + scm-mail-plugin + + +