diff --git a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java index 2021d4d00f..80ad1da7b8 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -47,19 +47,20 @@ public final class InstalledPlugin implements Plugin /** * Constructs a new plugin wrapper. - * - * @param descriptor wrapped plugin + * @param descriptor wrapped plugin * @param classLoader plugin class loader * @param webResourceLoader web resource loader * @param directory plugin directory + * @param core marked as core or not */ public InstalledPlugin(InstalledPluginDescriptor descriptor, ClassLoader classLoader, - WebResourceLoader webResourceLoader, Path directory) + WebResourceLoader webResourceLoader, Path directory, boolean core) { this.descriptor = descriptor; this.classLoader = classLoader; this.webResourceLoader = webResourceLoader; this.directory = directory; + this.core = core; } //~--- get methods ---------------------------------------------------------- @@ -120,6 +121,10 @@ public final class InstalledPlugin implements Plugin return webResourceLoader; } + public boolean isCore() { + return core; + } + //~--- fields --------------------------------------------------------------- /** plugin class loader */ @@ -133,4 +138,6 @@ public final class InstalledPlugin implements Plugin /** plugin web resource loader */ private final WebResourceLoader webResourceLoader; + + private final boolean core; } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java index 3ee4a4ea73..5d77470637 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/AvailablePluginResource.java @@ -4,7 +4,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import sonia.scm.plugin.AvailablePlugin; -import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginPermissions; import sonia.scm.web.VndMediaType; @@ -19,6 +19,7 @@ import javax.ws.rs.QueryParam; import javax.ws.rs.core.Response; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; import static sonia.scm.ContextEntry.ContextBuilder.entity; import static sonia.scm.NotFoundException.notFound; @@ -51,10 +52,15 @@ public class AvailablePluginResource { @Produces(VndMediaType.PLUGIN_COLLECTION) public Response getAvailablePlugins() { PluginPermissions.read().check(); - List available = pluginManager.getAvailable(); + List installed = pluginManager.getInstalled(); + List available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList()); return Response.ok(collectionMapper.mapAvailable(available)).build(); } + private boolean notInstalled(AvailablePlugin a, List installed) { + return installed.stream().noneMatch(installedPlugin -> installedPlugin.getDescriptor().getInformation().getName().equals(a.getDescriptor().getInformation().getName())); + } + /** * Returns available plugin. * @@ -95,16 +101,4 @@ public class AvailablePluginResource { pluginManager.install(name, restartAfterInstallation); return Response.ok().build(); } - - @POST - @Path("/install-pending") - @StatusCodes({ - @ResponseCode(code = 200, condition = "success"), - @ResponseCode(code = 500, condition = "internal server error") - }) - public Response installPending() { - PluginPermissions.manage().check(); - pluginManager.installPendingAndRestart(); - return Response.ok().build(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java index d05596abd5..b54c831662 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -54,6 +54,9 @@ public class IndexDtoGenerator extends HalAppenderMapper { builder.single(link("installedPlugins", resourceLinks.installedPluginCollection().self())); builder.single(link("availablePlugins", resourceLinks.availablePluginCollection().self())); } + if (PluginPermissions.manage().isPermitted()) { + builder.single(link("pendingPlugins", resourceLinks.pendingPluginCollection().self())); + } if (UserPermissions.list().isPermitted()) { builder.single(link("users", resourceLinks.userCollection().self())); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java index bc9d4b397c..5b5d0f267b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InstalledPluginResource.java @@ -3,8 +3,8 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.InstalledPlugin; -import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginPermissions; import sonia.scm.web.VndMediaType; @@ -50,7 +50,8 @@ public class InstalledPluginResource { public Response getInstalledPlugins() { PluginPermissions.read().check(); List plugins = pluginManager.getInstalled(); - return Response.ok(collectionMapper.mapInstalled(plugins)).build(); + List available = pluginManager.getAvailable(); + return Response.ok(collectionMapper.mapInstalled(plugins, available)).build(); } /** @@ -72,8 +73,9 @@ public class InstalledPluginResource { public Response getInstalledPlugin(@PathParam("name") String name) { PluginPermissions.read().check(); Optional pluginDto = pluginManager.getInstalled(name); + List available = pluginManager.getAvailable(); if (pluginDto.isPresent()) { - return Response.ok(mapper.mapInstalled(pluginDto.get())).build(); + return Response.ok(mapper.mapInstalled(pluginDto.get(), available)).build(); } else { throw notFound(entity("Plugin", name)); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java new file mode 100644 index 0000000000..47e3d2f9f6 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -0,0 +1,105 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.PluginManager; +import sonia.scm.plugin.PluginPermissions; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static java.util.stream.Collectors.toList; + +public class PendingPluginResource { + + private final PluginManager pluginManager; + private final ResourceLinks resourceLinks; + private final PluginDtoMapper mapper; + + @Inject + public PendingPluginResource(PluginManager pluginManager, ResourceLinks resourceLinks, PluginDtoMapper mapper) { + this.pluginManager = pluginManager; + this.resourceLinks = resourceLinks; + this.mapper = mapper; + } + + @GET + @Path("") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.PLUGIN_COLLECTION) + public Response getPending() { + PluginPermissions.manage().check(); + + List pending = pluginManager + .getAvailable() + .stream() + .filter(AvailablePlugin::isPending) + .collect(toList()); + List installed = pluginManager.getInstalled(); + + Stream newPlugins = pending + .stream() + .filter(a -> !contains(installed, a)); + Stream updatePlugins = installed + .stream() + .filter(i -> contains(pending, i)); + + Links.Builder linksBuilder = linkingTo().self(resourceLinks.pendingPluginCollection().self()); + + if (!pending.isEmpty()) { + linksBuilder.single(link("install", resourceLinks.pendingPluginCollection().installPending())); + } + + Embedded.Builder embedded = Embedded.embeddedBuilder(); + embedded.with("new", newPlugins.map(mapper::mapAvailable).collect(toList())); + embedded.with("update", updatePlugins.map(i -> mapper.mapInstalled(i, pending)).collect(toList())); + + return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build(); + } + + private boolean contains(Collection installedPlugins, AvailablePlugin availablePlugin) { + return installedPlugins + .stream() + .anyMatch(installedPlugin -> haveSameName(installedPlugin, availablePlugin)); + } + + private boolean contains(Collection availablePlugins, InstalledPlugin installedPlugin) { + return availablePlugins + .stream() + .anyMatch(availablePlugin -> haveSameName(installedPlugin, availablePlugin)); + } + + private boolean haveSameName(InstalledPlugin installedPlugin, AvailablePlugin availablePlugin) { + return installedPlugin.getDescriptor().getInformation().getName().equals(availablePlugin.getDescriptor().getInformation().getName()); + } + + @POST + @Path("/install") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 500, condition = "internal server error") + }) + public Response installPending() { + PluginPermissions.manage().check(); + pluginManager.installPendingAndRestart(); + return Response.ok().build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index bf20d1b67e..d9782643b2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; @@ -16,12 +17,16 @@ public class PluginDto extends HalRepresentation { private String name; private String version; + @JsonInclude(JsonInclude.Include.NON_NULL) + private String newVersion; private String displayName; private String description; private String author; private String category; private String avatarUrl; private boolean pending; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Boolean core; private Set dependencies; public PluginDto(Links links) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java index 7c1ee3d5a6..3d817625c7 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java @@ -26,8 +26,11 @@ public class PluginDtoCollectionMapper { this.mapper = mapper; } - public HalRepresentation mapInstalled(List plugins) { - List dtos = plugins.stream().map(mapper::mapInstalled).collect(toList()); + public HalRepresentation mapInstalled(List plugins, List availablePlugins) { + List dtos = plugins + .stream() + .map(i -> mapper.mapInstalled(i, availablePlugins)) + .collect(toList()); return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos)); } @@ -50,10 +53,6 @@ public class PluginDtoCollectionMapper { Links.Builder linksBuilder = linkingTo() .with(Links.linkingTo().self(baseUrl).build()); - if (PluginPermissions.manage().isPermitted() && containsPending(plugins)) { - linksBuilder.single(Link.link("installPending", resourceLinks.availablePluginCollection().installPending())); - } - return linksBuilder.build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index 25faf0a101..222e34832a 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -11,6 +11,9 @@ import sonia.scm.plugin.PluginPermissions; import javax.inject.Inject; +import java.util.List; +import java.util.Optional; + import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @@ -22,8 +25,8 @@ public abstract class PluginDtoMapper { public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto); - public PluginDto mapInstalled(InstalledPlugin plugin) { - PluginDto dto = createDtoForInstalled(plugin); + public PluginDto mapInstalled(InstalledPlugin plugin, List availablePlugins) { + PluginDto dto = createDtoForInstalled(plugin, availablePlugins); map(dto, plugin); return dto; } @@ -57,13 +60,36 @@ public abstract class PluginDtoMapper { return new PluginDto(links.build()); } - private PluginDto createDtoForInstalled(InstalledPlugin plugin) { + private PluginDto createDtoForInstalled(InstalledPlugin plugin, List availablePlugins) { PluginInformation information = plugin.getDescriptor().getInformation(); + Optional availablePlugin = checkForUpdates(plugin, availablePlugins); Links.Builder links = linkingTo() .self(resourceLinks.installedPlugin() .self(information.getName())); + if (!plugin.isCore() + && availablePlugin.isPresent() + && !availablePlugin.get().isPending() + && PluginPermissions.manage().isPermitted() + ) { + links.single(link("update", resourceLinks.availablePlugin().install(information.getName()))); + } - return new PluginDto(links.build()); + PluginDto dto = new PluginDto(links.build()); + + availablePlugin.ifPresent(value -> { + dto.setNewVersion(value.getDescriptor().getInformation().getVersion()); + dto.setPending(value.isPending()); + }); + + dto.setCore(plugin.isCore()); + + return dto; + } + + private Optional checkForUpdates(InstalledPlugin plugin, List availablePlugins) { + return availablePlugins.stream() + .filter(a -> a.getDescriptor().getInformation().getName().equals(plugin.getDescriptor().getInformation().getName())) + .findAny(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java index 79c46369a3..14abbe73d8 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginRootResource.java @@ -9,11 +9,13 @@ public class PluginRootResource { private Provider installedPluginResourceProvider; private Provider availablePluginResourceProvider; + private Provider pendingPluginResourceProvider; @Inject - public PluginRootResource(Provider installedPluginResourceProvider, Provider availablePluginResourceProvider) { + public PluginRootResource(Provider installedPluginResourceProvider, Provider availablePluginResourceProvider, Provider pendingPluginResourceProvider) { this.installedPluginResourceProvider = installedPluginResourceProvider; this.availablePluginResourceProvider = availablePluginResourceProvider; + this.pendingPluginResourceProvider = pendingPluginResourceProvider; } @Path("/installed") @@ -23,4 +25,7 @@ public class PluginRootResource { @Path("/available") public AvailablePluginResource availablePlugins() { return availablePluginResourceProvider.get(); } + + @Path("/pending") + public PendingPluginResource pendingPlugins() { return pendingPluginResourceProvider.get(); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java index bf92c567cd..3a797734ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ResourceLinks.java @@ -715,12 +715,28 @@ class ResourceLinks { availablePluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, AvailablePluginResource.class); } + String self() { + return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugins").parameters().href(); + } + } + + public PendingPluginCollectionLinks pendingPluginCollection() { + return new PendingPluginCollectionLinks(scmPathInfoStore.get()); + } + + static class PendingPluginCollectionLinks { + private final LinkBuilder pendingPluginCollectionLinkBuilder; + + PendingPluginCollectionLinks(ScmPathInfo pathInfo) { + pendingPluginCollectionLinkBuilder = new LinkBuilder(pathInfo, PluginRootResource.class, PendingPluginResource.class); + } + String installPending() { - return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("installPending").parameters().href(); + return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("installPending").parameters().href(); } String self() { - return availablePluginCollectionLinkBuilder.method("availablePlugins").parameters().method("getAvailablePlugins").parameters().href(); + return pendingPluginCollectionLinkBuilder.method("pendingPlugins").parameters().method("getPending").parameters().href(); } } 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 11e42891e4..6c715c2842 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -39,8 +39,10 @@ import com.google.inject.Singleton; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import sonia.scm.version.Version; import javax.inject.Inject; import java.io.IOException; @@ -54,6 +56,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import static sonia.scm.ContextEntry.ContextBuilder.entity; +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; //~--- JDK imports ------------------------------------------------------------ @@ -97,7 +100,7 @@ public class DefaultPluginManager implements PluginManager { return center.getAvailable() .stream() .filter(filterByName(name)) - .filter(this::isNotInstalled) + .filter(this::isNotInstalledOrMoreUpToDate) .map(p -> getPending(name).orElse(p)) .findFirst(); } @@ -130,7 +133,7 @@ public class DefaultPluginManager implements PluginManager { PluginPermissions.read().check(); return center.getAvailable() .stream() - .filter(this::isNotInstalled) + .filter(this::isNotInstalledOrMoreUpToDate) .map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p)) .collect(Collectors.toList()); } @@ -139,13 +142,26 @@ public class DefaultPluginManager implements PluginManager { return plugin -> name.equals(plugin.getDescriptor().getInformation().getName()); } - private boolean isNotInstalled(AvailablePlugin availablePlugin) { - return !getInstalled(availablePlugin.getDescriptor().getInformation().getName()).isPresent(); + private boolean isNotInstalledOrMoreUpToDate(AvailablePlugin availablePlugin) { + return getInstalled(availablePlugin.getDescriptor().getInformation().getName()) + .map(installedPlugin -> availableIsMoreUpToDateThanInstalled(availablePlugin, installedPlugin)) + .orElse(true); + } + + private boolean availableIsMoreUpToDateThanInstalled(AvailablePlugin availablePlugin, InstalledPlugin installed) { + return Version.parse(availablePlugin.getDescriptor().getInformation().getVersion()).isNewer(installed.getDescriptor().getInformation().getVersion()); } @Override public void install(String name, boolean restartAfterInstallation) { PluginPermissions.manage().check(); + + getInstalled(name) + .map(InstalledPlugin::isCore) + .ifPresent( + core -> doThrow().violation("plugin is a core plugin and cannot be updated").when(core) + ); + List plugins = collectPluginsToInstall(name); List pendingInstallations = new ArrayList<>(); for (AvailablePlugin plugin : plugins) { @@ -200,22 +216,18 @@ public class DefaultPluginManager implements PluginManager { private List collectPluginsToInstall(String name) { List plugins = new ArrayList<>(); - collectPluginsToInstall(plugins, name); + collectPluginsToInstall(plugins, name, true); return plugins; } - private boolean isInstalledOrPending(String name) { - return getInstalled(name).isPresent() || getPending(name).isPresent(); - } - - private void collectPluginsToInstall(List plugins, String name) { - if (!isInstalledOrPending(name)) { + private void collectPluginsToInstall(List plugins, String name, boolean isUpdate) { + if (!isInstalledOrPending(name) || isUpdate && isUpdatable(name)) { AvailablePlugin plugin = getAvailable(name).orElseThrow(() -> NotFoundException.notFound(entity(AvailablePlugin.class, name))); Set dependencies = plugin.getDescriptor().getDependencies(); if (dependencies != null) { for (String dependency: dependencies){ - collectPluginsToInstall(plugins, dependency); + collectPluginsToInstall(plugins, dependency, false); } } @@ -224,4 +236,12 @@ public class DefaultPluginManager implements PluginManager { LOG.info("plugin {} is already installed or installation is pending, skipping installation", name); } } + + private boolean isInstalledOrPending(String name) { + return getInstalled(name).isPresent() || getPending(name).isPresent(); + } + + private boolean isUpdatable(String name) { + return getAvailable(name).isPresent() && !getPending(name).isPresent(); + } } 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 fced7a01ed..e1f0367948 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginProcessor.java @@ -461,13 +461,16 @@ public final class PluginProcessor Path descriptorPath = directory.resolve(PluginConstants.FILE_DESCRIPTOR); if (Files.exists(descriptorPath)) { + + boolean core = Files.exists(directory.resolve("core")); + ClassLoader cl = createClassLoader(classLoader, smp); InstalledPluginDescriptor descriptor = createDescriptor(cl, descriptorPath); WebResourceLoader resourceLoader = createWebResourceLoader(directory); - plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory); + plugin = new InstalledPlugin(descriptor, cl, resourceLoader, directory, core); } else { logger.warn("found plugin directory without plugin descriptor"); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java index c108d4ee7a..32eadf7af0 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; +import org.apache.shiro.ShiroException; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; import org.jboss.resteasy.core.Dispatcher; @@ -18,6 +19,8 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.PluginCondition; import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; @@ -25,7 +28,6 @@ import sonia.scm.web.VndMediaType; import javax.inject.Provider; import javax.servlet.http.HttpServletResponse; - import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.util.Collections; @@ -34,6 +36,9 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,9 +48,6 @@ class AvailablePluginResourceTest { private Dispatcher dispatcher; - @Mock - Provider installedPluginResourceProvider; - @Mock Provider availablePluginResourceProvider; @@ -63,13 +65,14 @@ class AvailablePluginResourceTest { PluginRootResource pluginRootResource; - private final Subject subject = mock(Subject.class); + @Mock + Subject subject; @BeforeEach void prepareEnvironment() { dispatcher = MockDispatcherFactory.createDispatcher(); - pluginRootResource = new PluginRootResource(installedPluginResourceProvider, availablePluginResourceProvider); + pluginRootResource = new PluginRootResource(null, availablePluginResourceProvider, null); when(availablePluginResourceProvider.get()).thenReturn(availablePluginResource); dispatcher.getRegistry().addSingletonResource(pluginRootResource); } @@ -80,7 +83,7 @@ class AvailablePluginResourceTest { @BeforeEach void bindSubject() { ThreadContext.bind(subject); - when(subject.isPermitted(any(String.class))).thenReturn(true); + doNothing().when(subject).checkPermission(any(String.class)); } @AfterEach @@ -90,9 +93,10 @@ class AvailablePluginResourceTest { @Test void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin plugin = createPlugin(); + AvailablePlugin plugin = createAvailablePlugin(); when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(plugin)); + when(pluginManager.getInstalled()).thenReturn(Collections.emptyList()); when(collectionMapper.mapAvailable(Collections.singletonList(plugin))).thenReturn(new MockedResultDto()); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); @@ -105,13 +109,32 @@ class AvailablePluginResourceTest { assertThat(response.getContentAsString()).contains("\"marker\":\"x\""); } + @Test + void shouldNotReturnInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin(); + InstalledPlugin installedPlugin = createInstalledPlugin(); + + when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(availablePlugin)); + when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); + lenient().when(collectionMapper.mapAvailable(Collections.singletonList(availablePlugin))).thenReturn(new MockedResultDto()); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); + request.accept(VndMediaType.PLUGIN_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); + assertThat(response.getContentAsString()).doesNotContain("\"marker\":\"x\""); + } + @Test void getAvailablePlugin() throws UnsupportedEncodingException, URISyntaxException { PluginInformation pluginInformation = new PluginInformation(); pluginInformation.setName("pluginName"); pluginInformation.setVersion("2.0.0"); - AvailablePlugin plugin = createPlugin(pluginInformation); + AvailablePlugin plugin = createAvailablePlugin(pluginInformation); when(pluginManager.getAvailable("pluginName")).thenReturn(Optional.of(plugin)); @@ -139,38 +162,46 @@ class AvailablePluginResourceTest { verify(pluginManager).install("pluginName", false); assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); } - - @Test - void installPendingPlugin() throws URISyntaxException { - MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/install-pending"); - MockHttpResponse response = new MockHttpResponse(); - - dispatcher.invoke(request, response); - - verify(pluginManager).installPendingAndRestart(); - assertThat(HttpServletResponse.SC_OK).isEqualTo(response.getStatus()); - } } - private AvailablePlugin createPlugin() { - return createPlugin(new PluginInformation()); + private AvailablePlugin createAvailablePlugin() { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setName("scm-some-plugin"); + return createAvailablePlugin(pluginInformation); } - private AvailablePlugin createPlugin(PluginInformation pluginInformation) { + private AvailablePlugin createAvailablePlugin(PluginInformation pluginInformation) { AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null ); return new AvailablePlugin(descriptor); } + private InstalledPlugin createInstalledPlugin() { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setName("scm-some-plugin"); + return createInstalledPlugin(pluginInformation); + } + + private InstalledPlugin createInstalledPlugin(PluginInformation pluginInformation) { + InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(pluginInformation); + return new InstalledPlugin(descriptor, null, null, null, false); + } + @Nested class WithoutAuthorization { @BeforeEach - void unbindSubject() { - ThreadContext.unbindSubject(); + void bindSubject() { + ThreadContext.bind(subject); + doThrow(new ShiroException()).when(subject).checkPermission(any(String.class)); } + @AfterEach + public void unbindSubject() { + ThreadContext.unbindSubject(); + } @Test void shouldNotGetAvailablePluginsIfMissingPermission() throws URISyntaxException { MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); @@ -178,6 +209,7 @@ class AvailablePluginResourceTest { MockHttpResponse response = new MockHttpResponse(); assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + verify(subject).checkPermission(any(String.class)); } @Test @@ -187,16 +219,17 @@ class AvailablePluginResourceTest { MockHttpResponse response = new MockHttpResponse(); assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + verify(subject).checkPermission(any(String.class)); } @Test void shouldNotInstallPluginIfMissingPermission() throws URISyntaxException { - ThreadContext.unbindSubject(); MockHttpRequest request = MockHttpRequest.post("/v2/plugins/available/pluginName/install"); request.accept(VndMediaType.PLUGIN); MockHttpResponse response = new MockHttpResponse(); assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + verify(subject).checkPermission(any(String.class)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java index 8971ab5a8d..e2a23f0d52 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/InstalledPluginResourceTest.java @@ -29,6 +29,7 @@ import java.net.URISyntaxException; import java.util.Collections; import java.util.Optional; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -65,7 +66,7 @@ class InstalledPluginResourceTest { @BeforeEach void prepareEnvironment() { dispatcher = MockDispatcherFactory.createDispatcher(); - pluginRootResource = new PluginRootResource(installedPluginResourceProvider, availablePluginResourceProvider); + pluginRootResource = new PluginRootResource(installedPluginResourceProvider, null, null); when(installedPluginResourceProvider.get()).thenReturn(installedPluginResource); dispatcher.getRegistry().addSingletonResource(pluginRootResource); } @@ -88,7 +89,7 @@ class InstalledPluginResourceTest { void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { InstalledPlugin installedPlugin = createInstalled(""); when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); - when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin))).thenReturn(new MockedResultDto()); + when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin), Collections.emptyList())).thenReturn(new MockedResultDto()); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); request.accept(VndMediaType.PLUGIN_COLLECTION); @@ -111,7 +112,7 @@ class InstalledPluginResourceTest { PluginDto pluginDto = new PluginDto(); pluginDto.setName("pluginName"); - when(mapper.mapInstalled(installedPlugin)).thenReturn(pluginDto); + when(mapper.mapInstalled(installedPlugin, emptyList())).thenReturn(pluginDto); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed/pluginName"); request.accept(VndMediaType.PLUGIN); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java new file mode 100644 index 0000000000..d4b8a5bf5a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -0,0 +1,227 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.util.Providers; +import org.apache.shiro.ShiroException; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.util.ThreadContext; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.AvailablePluginDescriptor; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginInformation; +import sonia.scm.plugin.PluginManager; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; + +import static java.net.URI.create; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PendingPluginResourceTest { + + Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + ResourceLinks resourceLinks = ResourceLinksMock.createMock(create("/")); + + @Mock + PluginManager pluginManager; + @Mock + PluginDtoMapper mapper; + + @Mock + Subject subject; + + @InjectMocks + PendingPluginResource pendingPluginResource; + + MockHttpResponse response = new MockHttpResponse(); + + @BeforeEach + void prepareEnvironment() { + dispatcher = MockDispatcherFactory.createDispatcher(); + dispatcher.getProviderFactory().register(new PermissionExceptionMapper()); + PluginRootResource pluginRootResource = new PluginRootResource(null, null, Providers.of(pendingPluginResource)); + dispatcher.getRegistry().addSingletonResource(pluginRootResource); + } + + @BeforeEach + void mockMapper() { + lenient().when(mapper.mapAvailable(any())).thenAnswer(invocation -> { + PluginDto dto = new PluginDto(); + dto.setName(((AvailablePlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName()); + return dto; + }); + lenient().when(mapper.mapInstalled(any(), any())).thenAnswer(invocation -> { + PluginDto dto = new PluginDto(); + dto.setName(((InstalledPlugin)invocation.getArgument(0)).getDescriptor().getInformation().getName()); + return dto; + }); + } + + @Nested + class withAuthorization { + + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + doNothing().when(subject).checkPermission("plugin:manage"); + } + + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); + when(availablePlugin.isPending()).thenReturn(false); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); + assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); + } + + @Test + void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); + when(availablePlugin.isPending()).thenReturn(true); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"new\":[{\"name\":\"pending-available-plugin\""); + assertThat(response.getContentAsString()).contains("\"install\":{\"href\":\"/v2/plugins/pending/install\"}"); + System.out.println(response.getContentAsString()); + } + + @Test + void shouldGetPendingUpdatePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); + when(availablePlugin.isPending()).thenReturn(true); + when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); + InstalledPlugin installedPlugin = createInstalledPlugin("available-plugin"); + when(pluginManager.getInstalled()).thenReturn(singletonList(installedPlugin)); + + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + assertThat(response.getContentAsString()).contains("\"update\":[{\"name\":\"available-plugin\""); + assertThat(response.getContentAsString()).contains("\"install\":{\"href\":\"/v2/plugins/pending/install\"}"); + System.out.println(response.getContentAsString()); + } + + @Test + void shouldInstallPendingPlugins() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/install"); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + verify(pluginManager).installPendingAndRestart(); + } + } + + @Nested + class WithoutAuthorization { + + @BeforeEach + void bindSubject() { + ThreadContext.bind(subject); + doThrow(new ShiroException()).when(subject).checkPermission("plugin:manage"); + } + + @AfterEach + void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + void shouldNotListPendingPlugins() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.get("/v2/plugins/pending"); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + verify(pluginManager, never()).installPendingAndRestart(); + } + + @Test + void shouldNotInstallPendingPlugins() throws URISyntaxException { + MockHttpRequest request = MockHttpRequest.post("/v2/plugins/pending/install"); + + dispatcher.invoke(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + verify(pluginManager, never()).installPendingAndRestart(); + } + } + + static class PermissionExceptionMapper implements ExceptionMapper { + + @Override + public Response toResponse(ShiroException exception) { + return Response.status(401).entity(exception.getMessage()).build(); + } + } + + private AvailablePlugin createAvailablePlugin(String name) { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setName(name); + return createAvailablePlugin(pluginInformation); + } + + private AvailablePlugin createAvailablePlugin(PluginInformation pluginInformation) { + AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(pluginInformation); + AvailablePlugin availablePlugin = mock(AvailablePlugin.class); + lenient().when(availablePlugin.getDescriptor()).thenReturn(descriptor); + return availablePlugin; + } + + private InstalledPlugin createInstalledPlugin(String name) { + PluginInformation pluginInformation = new PluginInformation(); + pluginInformation.setName(name); + return createInstalledPlugin(pluginInformation); + } + + private InstalledPlugin createInstalledPlugin(PluginInformation pluginInformation) { + InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(pluginInformation); + InstalledPlugin installedPlugin = mock(InstalledPlugin.class); + lenient().when(installedPlugin.getDescriptor()).thenReturn(descriptor); + return installedPlugin; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java new file mode 100644 index 0000000000..fb368d12f2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java @@ -0,0 +1,171 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.plugin.AvailablePlugin; +import sonia.scm.plugin.AvailablePluginDescriptor; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginInformation; + +import java.net.URI; +import java.util.List; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class PluginDtoCollectionMapperTest { + + ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); + + @InjectMocks + PluginDtoMapperImpl pluginDtoMapper; + + Subject subject = mock(Subject.class); + ThreadState subjectThreadState = new SubjectThreadState(subject); + + @BeforeEach + void bindSubject() { + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @AfterEach + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + + @Test + void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() { + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(createAvailablePlugin("scm-other-plugin", "2"))); + + List plugins = result.getEmbedded().getItemsBy("plugins"); + assertThat(plugins).hasSize(1); + PluginDto plugin = (PluginDto) plugins.get(0); + assertThat(plugin.getVersion()).isEqualTo("1"); + assertThat(plugin.getNewVersion()).isNull(); + } + + @Test + void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() { + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.getVersion()).isEqualTo("1"); + assertThat(plugin.getNewVersion()).isEqualTo("2"); + } + + @Test + void shouldNotAddInstallLinkForNewVersionWhenNotPermitted() { + when(subject.isPermitted("plugin:manage")).thenReturn(false); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); + } + + @Test + void shouldNotAddInstallLinkForNewVersionWhenInstallationIsPending() { + when(subject.isPermitted("plugin:manage")).thenReturn(true); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); + when(availablePlugin.isPending()).thenReturn(true); + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(availablePlugin)); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); + } + + @Test + void shouldAddInstallLinkForNewVersionWhenPermitted() { + when(subject.isPermitted("plugin:manage")).thenReturn(true); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); + } + + @Test + void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() { + when(subject.isPermitted("plugin:manage")).thenReturn(true); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper); + + AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); + when(availablePlugin.isPending()).thenReturn(true); + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(availablePlugin)); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.isPending()).isTrue(); + } + + private PluginDto getPluginDtoFromResult(HalRepresentation result) { + assertThat(result.getEmbedded().getItemsBy("plugins")).hasSize(1); + List plugins = result.getEmbedded().getItemsBy("plugins"); + return (PluginDto) plugins.get(0); + } + + private InstalledPlugin createInstalledPlugin(String name, String version) { + PluginInformation information = new PluginInformation(); + information.setName(name); + information.setVersion(version); + return createInstalledPlugin(information); + } + + private InstalledPlugin createInstalledPlugin(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class); + InstalledPluginDescriptor descriptor = mock(InstalledPluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(information); + lenient().when(plugin.getDescriptor()).thenReturn(descriptor); + return plugin; + } + + private AvailablePlugin createAvailablePlugin(String name, String version) { + PluginInformation information = new PluginInformation(); + information.setName(name); + information.setVersion(version); + return createAvailablePlugin(information); + } + + private AvailablePlugin createAvailablePlugin(PluginInformation information) { + AvailablePlugin plugin = mock(AvailablePlugin.class); + AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(information); + lenient().when(plugin.getDescriptor()).thenReturn(descriptor); + return plugin; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java index 27e3ca32ea..5bd7c1199e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -17,7 +17,9 @@ import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginInformation; import java.net.URI; +import java.util.Collections; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -76,7 +78,7 @@ class PluginDtoMapperTest { void shouldAppendInstalledSelfLink() { InstalledPlugin plugin = createInstalled(createPluginInformation()); - PluginDto dto = mapper.mapInstalled(plugin); + PluginDto dto = mapper.mapInstalled(plugin, emptyList()); assertThat(dto.getLinks().getLinkBy("self").get().getHref()) .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin"); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index 1aef4e57cb..478b3efc92 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -38,6 +38,7 @@ public class ResourceLinksMock { when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.installedPluginCollection()).thenReturn(new ResourceLinks.InstalledPluginCollectionLinks(uriInfo)); when(resourceLinks.availablePluginCollection()).thenReturn(new ResourceLinks.AvailablePluginCollectionLinks(uriInfo)); + when(resourceLinks.pendingPluginCollection()).thenReturn(new ResourceLinks.PendingPluginCollectionLinks(uriInfo)); when(resourceLinks.installedPlugin()).thenReturn(new ResourceLinks.InstalledPluginLinks(uriInfo)); when(resourceLinks.availablePlugin()).thenReturn(new ResourceLinks.AvailablePluginLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java index 7cb534c7ba..438d1ab305 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultUberWebResourceLoaderTest.java @@ -248,7 +248,7 @@ public class DefaultUberWebResourceLoaderTest extends WebResourceLoaderTestBase private InstalledPlugin createPluginWrapper(Path directory) { return new InstalledPlugin(null, null, new PathWebResourceLoader(directory), - directory); + directory, false); } //~--- fields --------------------------------------------------------------- diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java index 7e3577d775..2bd3dce1c2 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java @@ -10,12 +10,14 @@ public class PluginTestHelper { public static AvailablePlugin createAvailable(String name) { PluginInformation information = new PluginInformation(); information.setName(name); + information.setVersion("1.0"); return createAvailable(information); } public static InstalledPlugin createInstalled(String name) { PluginInformation information = new PluginInformation(); information.setName(name); + information.setVersion("1.0"); return createInstalled(information); }