From 250503f74b3d2697c69d301edc35d146c9f98141 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 9 Sep 2019 08:31:37 +0200 Subject: [PATCH 01/48] select defaultBranch on sources and commits when not yet selected --- scm-ui/src/repos/containers/ChangesetsRoot.js | 21 +++++++++++++++++++ .../src/repos/sources/containers/Sources.js | 11 ++++------ 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/scm-ui/src/repos/containers/ChangesetsRoot.js b/scm-ui/src/repos/containers/ChangesetsRoot.js index 8e0b44afbe..db440b17c0 100644 --- a/scm-ui/src/repos/containers/ChangesetsRoot.js +++ b/scm-ui/src/repos/containers/ChangesetsRoot.js @@ -43,8 +43,29 @@ type Props = { class ChangesetsRoot extends React.Component { componentDidMount() { this.props.fetchBranches(this.props.repository); + this.redirectToDefaultBranch(); } + redirectToDefaultBranch = () => { + if (this.shouldRedirectToDefaultBranch()) { + const defaultBranches = this.props.branches.filter( + b => b.defaultBranch === true + ); + if (defaultBranches.length > 0) { + this.branchSelected(defaultBranches[0]); + } + } + }; + + shouldRedirectToDefaultBranch = () => { + return ( + this.props.branches && + this.props.branches.length > 0 && + this.props.selected !== + this.props.branches.filter(b => b.defaultBranch === true)[0] + ); + }; + stripEndingSlash = (url: string) => { if (url.endsWith("/")) { return url.substring(0, url.length - 1); diff --git a/scm-ui/src/repos/sources/containers/Sources.js b/scm-ui/src/repos/sources/containers/Sources.js index d09e18e5be..065dfb5a51 100644 --- a/scm-ui/src/repos/sources/containers/Sources.js +++ b/scm-ui/src/repos/sources/containers/Sources.js @@ -80,20 +80,17 @@ class Sources extends React.Component { } redirectToDefaultBranch = () => { - const { branches, baseUrl } = this.props; - if (this.shouldRedirect()) { + const { branches } = this.props; + if (this.shouldRedirectToDefaultBranch()) { const defaultBranches = branches.filter(b => b.defaultBranch); if (defaultBranches.length > 0) { - this.props.history.push( - `${baseUrl}/${encodeURIComponent(defaultBranches[0].name)}/` - ); - this.setState({ selectedBranch: defaultBranches[0] }); + this.branchSelected(defaultBranches[0]); } } }; - shouldRedirect = () => { + shouldRedirectToDefaultBranch = () => { const { branches, revision } = this.props; return branches && !revision; }; From 3fe37370a447fa81979d77a62eec69b7b37bd8ca Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Tue, 10 Sep 2019 16:36:32 +0200 Subject: [PATCH 02/48] remove unnecessary buttongroup --- .../packages/ui-components/src/Breadcrumb.js | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/Breadcrumb.js b/scm-ui-components/packages/ui-components/src/Breadcrumb.js index d2f3409af6..5b0f151c61 100644 --- a/scm-ui-components/packages/ui-components/src/Breadcrumb.js +++ b/scm-ui-components/packages/ui-components/src/Breadcrumb.js @@ -1,10 +1,9 @@ //@flow import React from "react"; -import { Link } from "react-router-dom"; -import type { Branch, Repository } from "@scm-manager/ui-types"; +import {Link} from "react-router-dom"; +import type {Branch, Repository} from "@scm-manager/ui-types"; import injectSheet from "react-jss"; -import { ExtensionPoint, binder } from "@scm-manager/ui-extensions"; -import {ButtonGroup} from "./buttons"; +import {binder, ExtensionPoint} from "@scm-manager/ui-extensions"; import classNames from "classnames"; type Props = { @@ -64,33 +63,48 @@ class Breadcrumb extends React.Component { } render() { - const { classes, baseUrl, branch, defaultBranch, branches, revision, path, repository } = this.props; + const { + classes, + baseUrl, + branch, + defaultBranch, + branches, + revision, + path, + repository + } = this.props; return ( <>
-

From 202a638a0ff933b07b82b01e5b8f9911536a8641 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Sep 2019 14:51:38 +0200 Subject: [PATCH 03/48] Show updatable plugins --- .../v2/resources/InstalledPluginResource.java | 8 +- .../sonia/scm/api/v2/resources/PluginDto.java | 3 + .../resources/PluginDtoCollectionMapper.java | 7 +- .../scm/api/v2/resources/PluginDtoMapper.java | 31 +++- .../scm/plugin/DefaultPluginManager.java | 15 +- .../InstalledPluginResourceTest.java | 5 +- .../PluginDtoCollectionMapperTest.java | 171 ++++++++++++++++++ .../api/v2/resources/PluginDtoMapperTest.java | 4 +- .../scm/plugin/DefaultPluginManagerTest.java | 84 +++++---- 9 files changed, 277 insertions(+), 51 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java 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/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index bf20d1b67e..41d285d59c 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,6 +17,8 @@ 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; 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..e8e85a0054 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)); } 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..00b363c6a3 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,33 @@ 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 (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()); + }); + + 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/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index 807eaac317..9334f87822 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 org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import sonia.scm.version.Version; //~--- JDK imports ------------------------------------------------------------ import javax.inject.Inject; @@ -83,7 +84,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(); } @@ -116,7 +117,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()); } @@ -125,8 +126,14 @@ 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 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 7fa0081c5c..84cd827932 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; @@ -87,7 +88,7 @@ class InstalledPluginResourceTest { void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { InstalledPlugin installedPlugin = createPlugin(); 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); @@ -110,7 +111,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/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 5cf6bdd45a..1dc5e3d135 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; @@ -74,7 +76,7 @@ class PluginDtoMapperTest { void shouldAppendInstalledSelfLink() { InstalledPlugin plugin = createInstalled(); - 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/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 322163ee1a..705319f63f 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -23,7 +23,6 @@ import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; @@ -71,8 +70,8 @@ class DefaultPluginManagerTest { @Test void shouldReturnInstalledPlugins() { - InstalledPlugin review = createInstalled("scm-review-plugin"); - InstalledPlugin git = createInstalled("scm-git-plugin"); + InstalledPlugin review = createInstalled("scm-review-plugin", "1"); + InstalledPlugin git = createInstalled("scm-git-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); @@ -82,8 +81,8 @@ class DefaultPluginManagerTest { @Test void shouldReturnReviewPlugin() { - InstalledPlugin review = createInstalled("scm-review-plugin"); - InstalledPlugin git = createInstalled("scm-git-plugin"); + InstalledPlugin review = createInstalled("scm-review-plugin", "1"); + InstalledPlugin git = createInstalled("scm-git-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); @@ -101,8 +100,8 @@ class DefaultPluginManagerTest { @Test void shouldReturnAvailablePlugins() { - AvailablePlugin review = createAvailable("scm-review-plugin"); - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); @@ -111,22 +110,35 @@ class DefaultPluginManagerTest { } @Test - void shouldFilterOutAllInstalled() { - InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + void shouldFilterOutAllInstalledWithSameVersion() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); - AvailablePlugin review = createAvailable("scm-review-plugin"); - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); List available = manager.getAvailable(); assertThat(available).containsOnly(review); } + @Test + void shouldKeepInstalledWithOlderVersion() { + InstalledPlugin installedGit = createInstalled("scm-git-plugin", "1"); + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); + + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1.1"); + when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); + + List available = manager.getAvailable(); + assertThat(available).contains(git, review); + } + @Test void shouldReturnAvailable() { - AvailablePlugin review = createAvailable("scm-review-plugin"); - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, git)); Optional available = manager.getAvailable("scm-git-plugin"); @@ -135,7 +147,7 @@ class DefaultPluginManagerTest { @Test void shouldReturnEmptyForNonExistingAvailable() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); Optional available = manager.getAvailable("scm-git-plugin"); @@ -144,10 +156,10 @@ class DefaultPluginManagerTest { @Test void shouldReturnEmptyForInstalledPlugin() { - InstalledPlugin installedGit = createInstalled("scm-git-plugin"); + InstalledPlugin installedGit = createInstalled("scm-git-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedGit)); - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); Optional available = manager.getAvailable("scm-git-plugin"); @@ -156,7 +168,7 @@ class DefaultPluginManagerTest { @Test void shouldInstallThePlugin() { - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); manager.install("scm-git-plugin", false); @@ -167,9 +179,9 @@ class DefaultPluginManagerTest { @Test void shouldInstallDependingPlugins() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); - AvailablePlugin mail = createAvailable("scm-mail-plugin"); + AvailablePlugin mail = createAvailable("scm-mail-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); manager.install("scm-review-plugin", false); @@ -180,12 +192,12 @@ class DefaultPluginManagerTest { @Test void shouldNotInstallAlreadyInstalledDependencies() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); - AvailablePlugin mail = createAvailable("scm-mail-plugin"); + AvailablePlugin mail = createAvailable("scm-mail-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); - InstalledPlugin installedMail = createInstalled("scm-mail-plugin"); + InstalledPlugin installedMail = createInstalled("scm-mail-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(installedMail)); manager.install("scm-review-plugin", false); @@ -198,11 +210,11 @@ class DefaultPluginManagerTest { @Test void shouldRollbackOnFailedInstallation() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); - AvailablePlugin mail = createAvailable("scm-mail-plugin"); + AvailablePlugin mail = createAvailable("scm-mail-plugin", "1"); when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); - AvailablePlugin notification = createAvailable("scm-notification-plugin"); + AvailablePlugin notification = createAvailable("scm-notification-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail, notification)); PendingPluginInstallation pendingNotification = mock(PendingPluginInstallation.class); @@ -221,9 +233,9 @@ class DefaultPluginManagerTest { @Test void shouldInstallNothingIfOneOfTheDependenciesIsNotAvailable() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(review.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-mail-plugin")); - AvailablePlugin mail = createAvailable("scm-mail-plugin"); + AvailablePlugin mail = createAvailable("scm-mail-plugin", "1"); when(mail.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("scm-notification-plugin")); when(center.getAvailable()).thenReturn(ImmutableSet.of(review, mail)); @@ -234,7 +246,7 @@ class DefaultPluginManagerTest { @Test void shouldSendRestartEventAfterInstallation() { - AvailablePlugin git = createAvailable("scm-git-plugin"); + AvailablePlugin git = createAvailable("scm-git-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(git)); manager.install("scm-git-plugin", true); @@ -245,7 +257,7 @@ class DefaultPluginManagerTest { @Test void shouldNotSendRestartEventIfNoPluginWasInstalled() { - InstalledPlugin gitInstalled = createInstalled("scm-git-plugin"); + InstalledPlugin gitInstalled = createInstalled("scm-git-plugin", "1"); when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled)); manager.install("scm-git-plugin", true); @@ -254,7 +266,7 @@ class DefaultPluginManagerTest { @Test void shouldNotInstallAlreadyPendingPlugins() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); manager.install("scm-review-plugin", false); @@ -265,7 +277,7 @@ class DefaultPluginManagerTest { @Test void shouldSendRestartEvent() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); manager.install("scm-review-plugin", false); @@ -283,7 +295,7 @@ class DefaultPluginManagerTest { @Test void shouldReturnSingleAvailableAsPending() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); manager.install("scm-review-plugin", false); @@ -294,7 +306,7 @@ class DefaultPluginManagerTest { @Test void shouldReturnAvailableAsPending() { - AvailablePlugin review = createAvailable("scm-review-plugin"); + AvailablePlugin review = createAvailable("scm-review-plugin", "1"); when(center.getAvailable()).thenReturn(ImmutableSet.of(review)); manager.install("scm-review-plugin", false); @@ -355,15 +367,17 @@ class DefaultPluginManagerTest { } - private AvailablePlugin createAvailable(String name) { + private AvailablePlugin createAvailable(String name, String version) { PluginInformation information = new PluginInformation(); information.setName(name); + information.setVersion(version); return createAvailable(information); } - private InstalledPlugin createInstalled(String name) { + private InstalledPlugin createInstalled(String name, String version) { PluginInformation information = new PluginInformation(); information.setName(name); + information.setVersion(version); return createInstalled(information); } From ba68cec4fa07148b9a71b290a15905d793a0b6d2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Wed, 11 Sep 2019 14:54:07 +0200 Subject: [PATCH 04/48] add validator for filepath --- .../packages/ui-components/src/validation.js | 8 +++++++- scm-ui/src/users/components/userValidation.js | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/validation.js b/scm-ui-components/packages/ui-components/src/validation.js index fcfffcee45..221f9f12ea 100644 --- a/scm-ui-components/packages/ui-components/src/validation.js +++ b/scm-ui-components/packages/ui-components/src/validation.js @@ -5,7 +5,7 @@ export const isNameValid = (name: string) => { return nameRegex.test(name); }; -const mailRegex = /^[ -~]+@[A-Za-z0-9][\w\-.]*\.[A-Za-z0-9][A-Za-z0-9-]+$/; +const mailRegex = /^[ -~]+@[A-Za-z0-9][\w\-.]*\.[A-Za-z0-9][A-Za-z0-9-]+$/; export const isMailValid = (mail: string) => { return mailRegex.test(mail); @@ -14,3 +14,9 @@ export const isMailValid = (mail: string) => { export const isNumberValid = (number: string) => { return !isNaN(number); }; + +const pathRegex = /^((?!\/{2,}).)*$/; + +export const isValidPath = (path: string) => { + return pathRegex.test(path); +}; diff --git a/scm-ui/src/users/components/userValidation.js b/scm-ui/src/users/components/userValidation.js index c9460fdd50..8078df4bcd 100644 --- a/scm-ui/src/users/components/userValidation.js +++ b/scm-ui/src/users/components/userValidation.js @@ -2,9 +2,9 @@ import { validation } from "@scm-manager/ui-components"; -const { isNameValid, isMailValid } = validation; +const { isNameValid, isMailValid, isValidPath } = validation; -export { isNameValid, isMailValid }; +export { isNameValid, isMailValid, isValidPath }; export const isDisplayNameValid = (displayName: string) => { if (displayName) { From a7cb1d311621538761f506aeb944f515f97d2f4f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Sep 2019 15:05:25 +0200 Subject: [PATCH 05/48] Make updatable plugins installable --- .../scm/plugin/DefaultPluginManager.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) 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 9334f87822..56850758a2 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -178,22 +178,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); } } @@ -202,4 +198,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(); + } } From 0fdd1cea178e67357652c50a15640c327c6dd3b8 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Sep 2019 16:46:27 +0200 Subject: [PATCH 06/48] Detect core plugins and prevent installation --- .../sonia/scm/plugin/InstalledPlugin.java | 13 +++++++--- .../sonia/scm/api/v2/resources/PluginDto.java | 2 ++ .../scm/api/v2/resources/PluginDtoMapper.java | 5 +++- .../scm/plugin/DefaultPluginManager.java | 9 +++++++ .../sonia/scm/plugin/PluginProcessor.java | 5 +++- .../scm/plugin/DefaultPluginManagerTest.java | 26 +++++++++++++++++++ .../DefaultUberWebResourceLoaderTest.java | 2 +- 7 files changed, 56 insertions(+), 6 deletions(-) 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/PluginDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDto.java index 41d285d59c..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 @@ -25,6 +25,8 @@ public class PluginDto extends HalRepresentation { 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/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index 00b363c6a3..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 @@ -67,7 +67,8 @@ public abstract class PluginDtoMapper { Links.Builder links = linkingTo() .self(resourceLinks.installedPlugin() .self(information.getName())); - if (availablePlugin.isPresent() + if (!plugin.isCore() + && availablePlugin.isPresent() && !availablePlugin.get().isPending() && PluginPermissions.manage().isPermitted() ) { @@ -81,6 +82,8 @@ public abstract class PluginDtoMapper { dto.setPending(value.isPending()); }); + dto.setCore(plugin.isCore()); + return dto; } 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 56850758a2..2c3e92d618 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -40,6 +40,7 @@ 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; @@ -54,6 +55,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; /** * @@ -139,6 +141,13 @@ public class DefaultPluginManager implements PluginManager { @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) { 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/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index 705319f63f..6551b7d51d 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -16,6 +16,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; @@ -177,6 +178,31 @@ class DefaultPluginManagerTest { verify(eventBus, never()).post(any()); } + @Test + void shouldUpdateNormalPlugin() { + AvailablePlugin available = createAvailable("scm-git-plugin", "2"); + InstalledPlugin installed = createInstalled("scm-git-plugin", "1"); + when(installed.isCore()).thenReturn(false); + lenient().when(center.getAvailable()).thenReturn(ImmutableSet.of(available)); + when(loader.getInstalledPlugins()).thenReturn(ImmutableSet.of(installed)); + + manager.install("scm-git-plugin", false); + + verify(installer).install(available); + verify(eventBus, never()).post(any()); + } + + @Test + void shouldNotUpdateCorePlugin() { + AvailablePlugin available = createAvailable("scm-git-plugin", "2"); + InstalledPlugin installed = createInstalled("scm-git-plugin", "1"); + when(installed.isCore()).thenReturn(true); + lenient().when(center.getAvailable()).thenReturn(ImmutableSet.of(available)); + when(loader.getInstalledPlugins()).thenReturn(ImmutableSet.of(installed)); + + assertThrows(ScmConstraintViolationException.class, () -> manager.install("scm-git-plugin", false)); + } + @Test void shouldInstallDependingPlugins() { AvailablePlugin review = createAvailable("scm-review-plugin", "1"); 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 --------------------------------------------------------------- From 67d924f5b4ee1f5739cac10fe3dd62d25cf6e913 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Wed, 11 Sep 2019 17:01:47 +0200 Subject: [PATCH 07/48] Do not list available plugins in REST API when they are installed --- .../v2/resources/AvailablePluginResource.java | 10 +++- .../AvailablePluginResourceTest.java | 48 ++++++++++++++++--- 2 files changed, 50 insertions(+), 8 deletions(-) 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..dab5c2652e 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. * 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..436e0b630a 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 @@ -18,6 +18,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 +27,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 +35,7 @@ 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.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -90,9 +92,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 +108,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)); @@ -152,17 +174,31 @@ class AvailablePluginResourceTest { } } - 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 { From 6cad53ec875d283b7ca3d236a778da310b03f1b3 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Sep 2019 11:29:42 +0200 Subject: [PATCH 08/48] Add unit test --- .../ui-components/src/validation.test.js | 183 ++++++++++-------- 1 file changed, 99 insertions(+), 84 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/validation.test.js b/scm-ui-components/packages/ui-components/src/validation.test.js index d50996ff2b..6348bd67ba 100644 --- a/scm-ui-components/packages/ui-components/src/validation.test.js +++ b/scm-ui-components/packages/ui-components/src/validation.test.js @@ -2,102 +2,117 @@ import * as validator from "./validation"; describe("test name validation", () => { - it("should return false", () => { - // invalid names taken from ValidationUtilTest.java - const invalidNames = [ - "@test", - " test 123", - " test 123 ", - "test 123 ", - "test/123", - "test%123", - "test:123", - "t ", - " t", - " t ", - "", + // invalid names taken from ValidationUtilTest.java + const invalidNames = [ + "@test", + " test 123", + " test 123 ", + "test 123 ", + "test/123", + "test%123", + "test:123", + "t ", + " t", + " t ", + "", - " invalid_name", - "another%one", - "!!!", - "!_!" - ]; - for (let name of invalidNames) { + " invalid_name", + "another%one", + "!!!", + "!_!" + ]; + for (let name of invalidNames) { + it(`should return false for '${name}'`, () => { expect(validator.isNameValid(name)).toBe(false); - } - }); + }); + } - it("should return true", () => { - // valid names taken from ValidationUtilTest.java - const validNames = [ - "test", - "test.git", - "Test123.git", - "Test123-git", - "Test_user-123.git", - "test@scm-manager.de", - "test123", - "tt", - "t", - "valid_name", - "another1", - "stillValid", - "this.one_as-well", - "and@this" - ]; - for (let name of validNames) { + // valid names taken from ValidationUtilTest.java + const validNames = [ + "test", + "test.git", + "Test123.git", + "Test123-git", + "Test_user-123.git", + "test@scm-manager.de", + "test123", + "tt", + "t", + "valid_name", + "another1", + "stillValid", + "this.one_as-well", + "and@this" + ]; + for (let name of validNames) { + it(`should return true for '${name}'`, () => { expect(validator.isNameValid(name)).toBe(true); - } - }); + }); + } }); describe("test mail validation", () => { - it("should return false", () => { - // invalid taken from ValidationUtilTest.java - const invalid = [ - "ostfalia.de", - "@ostfalia.de", - "s.sdorra@", - "s.sdorra@ostfalia", - "s.sdorra@ ostfalia.de", - "s.sdorra@[ostfalia.de" - ]; - for (let mail of invalid) { + // invalid taken from ValidationUtilTest.java + const invalid = [ + "ostfalia.de", + "@ostfalia.de", + "s.sdorra@", + "s.sdorra@ostfalia", + "s.sdorra@ ostfalia.de", + "s.sdorra@[ostfalia.de" + ]; + for (let mail of invalid) { + it(`should return false for '${mail}'`, () => { expect(validator.isMailValid(mail)).toBe(false); - } - }); + }); + } - it("should return true", () => { - // valid taken from ValidationUtilTest.java - const valid = [ - "s.sdorra@ostfalia.de", - "sdorra@ostfalia.de", - "s.sdorra@hbk-bs.de", - "s.sdorra@gmail.com", - "s.sdorra@t.co", - "s.sdorra@ucla.college", - "s.sdorra@example.xn--p1ai", - "s.sdorra@scm.solutions", - "s'sdorra@scm.solutions", - "\"S Sdorra\"@scm.solutions" - ]; - for (let mail of valid) { + // valid taken from ValidationUtilTest.java + const valid = [ + "s.sdorra@ostfalia.de", + "sdorra@ostfalia.de", + "s.sdorra@hbk-bs.de", + "s.sdorra@gmail.com", + "s.sdorra@t.co", + "s.sdorra@ucla.college", + "s.sdorra@example.xn--p1ai", + "s.sdorra@scm.solutions", + "s'sdorra@scm.solutions", + "\"S Sdorra\"@scm.solutions" + ]; + for (let mail of valid) { + it(`should return true for '${mail}'`, () => { expect(validator.isMailValid(mail)).toBe(true); - } - }); + }); + } }); describe("test number validation", () => { - it("should return false", () => { - const invalid = ["1a", "35gu", "dj6", "45,5", "test"]; - for (let number of invalid) { - expect(validator.isNumberValid(number)).toBe(false); - } - }); - it("should return true", () => { - const valid = ["1", "35", "2", "235", "34.4"]; - for (let number of valid) { + const invalid = ["1a", "35gu", "dj6", "45,5", "test"]; + for (let number of invalid) { + it(`should return false for '${number}'`, () => { + expect(validator.isNumberValid(number)).toBe(false); + }); + } + const valid = ["1", "35", "2", "235", "34.4"]; + for (let number of valid) { + it(`should return true for '${number}'`, () => { expect(validator.isNumberValid(number)).toBe(true); - } - }); + }); + } +}); + +describe("test path validation", () => { + const invalid = ["//", "some//path", "end//"]; + for (let path of invalid) { + it(`should return false for '${path}'`, () => { + expect(validator.isValidPath(path)).toBe(false); + }); + } + const valid = ["", "/", "dir", "some/path", "end/"]; + for (let path of valid) { + it(`should return true for '${path}'`, () => { + expect(validator.isValidPath(path)).toBe(true); + }); + } }); From 7807f5d49db5f96279b6c72414e96c32700109c7 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 12 Sep 2019 11:36:49 +0200 Subject: [PATCH 09/48] rename path validation method --- scm-ui-components/packages/ui-components/src/validation.js | 2 +- scm-ui/src/users/components/userValidation.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/validation.js b/scm-ui-components/packages/ui-components/src/validation.js index 221f9f12ea..98a36c6f0e 100644 --- a/scm-ui-components/packages/ui-components/src/validation.js +++ b/scm-ui-components/packages/ui-components/src/validation.js @@ -17,6 +17,6 @@ export const isNumberValid = (number: string) => { const pathRegex = /^((?!\/{2,}).)*$/; -export const isValidPath = (path: string) => { +export const isPathValid = (path: string) => { return pathRegex.test(path); }; diff --git a/scm-ui/src/users/components/userValidation.js b/scm-ui/src/users/components/userValidation.js index 8078df4bcd..e53dce229c 100644 --- a/scm-ui/src/users/components/userValidation.js +++ b/scm-ui/src/users/components/userValidation.js @@ -2,9 +2,9 @@ import { validation } from "@scm-manager/ui-components"; -const { isNameValid, isMailValid, isValidPath } = validation; +const { isNameValid, isMailValid, isPathValid } = validation; -export { isNameValid, isMailValid, isValidPath }; +export { isNameValid, isMailValid, isPathValid }; export const isDisplayNameValid = (displayName: string) => { if (displayName) { From 17125d9c338396352958640ded3f8186d8d784f0 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Thu, 12 Sep 2019 11:39:04 +0200 Subject: [PATCH 10/48] Fix unit test --- .../packages/ui-components/src/validation.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scm-ui-components/packages/ui-components/src/validation.test.js b/scm-ui-components/packages/ui-components/src/validation.test.js index 6348bd67ba..4af5d755d0 100644 --- a/scm-ui-components/packages/ui-components/src/validation.test.js +++ b/scm-ui-components/packages/ui-components/src/validation.test.js @@ -106,13 +106,13 @@ describe("test path validation", () => { const invalid = ["//", "some//path", "end//"]; for (let path of invalid) { it(`should return false for '${path}'`, () => { - expect(validator.isValidPath(path)).toBe(false); + expect(validator.isPathValid(path)).toBe(false); }); } const valid = ["", "/", "dir", "some/path", "end/"]; for (let path of valid) { it(`should return true for '${path}'`, () => { - expect(validator.isValidPath(path)).toBe(true); + expect(validator.isPathValid(path)).toBe(true); }); } }); From a9fbd5f21ac1d786780323a2fc18b712a8848cc2 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Thu, 12 Sep 2019 14:44:42 +0200 Subject: [PATCH 11/48] remove buttongroup from extensionpoint --- .../src/repos/sources/containers/Content.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/scm-ui/src/repos/sources/containers/Content.js b/scm-ui/src/repos/sources/containers/Content.js index b9032ae681..4d297c05bc 100644 --- a/scm-ui/src/repos/sources/containers/Content.js +++ b/scm-ui/src/repos/sources/containers/Content.js @@ -106,17 +106,15 @@ class Content extends React.Component {
{selector}
- - - +
From b7aa2ea1cd8a8283c43b1cdb4143e06e90b36eb4 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 12:01:07 +0200 Subject: [PATCH 12/48] Fix unit test --- .../AvailablePluginResourceTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) 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 436e0b630a..705e13c3ff 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; @@ -35,6 +36,8 @@ 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; @@ -65,7 +68,8 @@ class AvailablePluginResourceTest { PluginRootResource pluginRootResource; - private final Subject subject = mock(Subject.class); + @Mock + Subject subject; @BeforeEach @@ -82,7 +86,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 @@ -203,10 +207,15 @@ class AvailablePluginResourceTest { 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"); @@ -214,6 +223,7 @@ class AvailablePluginResourceTest { MockHttpResponse response = new MockHttpResponse(); assertThrows(UnhandledException.class, () -> dispatcher.invoke(request, response)); + verify(subject).checkPermission(any(String.class)); } @Test @@ -223,16 +233,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)); } } From 0ceb1ad295bf9ae0c93dea7b598a4a25b1b9cd27 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 14:17:06 +0200 Subject: [PATCH 13/48] Add dedicated endpoint for pending plugins --- .../v2/resources/AvailablePluginResource.java | 12 -- .../api/v2/resources/IndexDtoGenerator.java | 3 + .../v2/resources/PendingPluginResource.java | 77 +++++++ .../resources/PluginDtoCollectionMapper.java | 4 - .../api/v2/resources/PluginRootResource.java | 7 +- .../scm/api/v2/resources/ResourceLinks.java | 20 +- .../AvailablePluginResourceTest.java | 16 +- .../InstalledPluginResourceTest.java | 2 +- .../resources/PendingPluginResourceTest.java | 192 ++++++++++++++++++ .../api/v2/resources/ResourceLinksMock.java | 1 + 10 files changed, 299 insertions(+), 35 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java create mode 100644 scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java 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 dab5c2652e..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 @@ -101,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/PendingPluginResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java new file mode 100644 index 0000000000..c9d9982ccc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PendingPluginResource.java @@ -0,0 +1,77 @@ +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.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.List; + +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()); + + 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("available", pending.stream().map(mapper::mapAvailable).collect(toList())); + + return Response.ok(new HalRepresentation(linksBuilder.build(), embedded.build())).build(); + } + + @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/PluginDtoCollectionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapper.java index e8e85a0054..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 @@ -53,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/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/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/AvailablePluginResourceTest.java index 705e13c3ff..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 @@ -48,9 +48,6 @@ class AvailablePluginResourceTest { private Dispatcher dispatcher; - @Mock - Provider installedPluginResourceProvider; - @Mock Provider availablePluginResourceProvider; @@ -75,7 +72,7 @@ class AvailablePluginResourceTest { @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); } @@ -165,17 +162,6 @@ 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 createAvailablePlugin() { 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 84cd827932..d231bc2cef 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 @@ -65,7 +65,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); } 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..065ff9d723 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PendingPluginResourceTest.java @@ -0,0 +1,192 @@ +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.PluginCondition; +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 java.util.Collections; + +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; + }); + } + + @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-available-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-available-plugin"); + } + + @Test + void shouldGetPendingAvailablePluginListsWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("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("\"_embedded\":{\"available\":[{\"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 = new AvailablePluginDescriptor( + pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null + ); + AvailablePlugin availablePlugin = mock(AvailablePlugin.class); + lenient().when(availablePlugin.getDescriptor()).thenReturn(descriptor); + return availablePlugin; + } +} 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)); From 2e42d8be915db7601651216eb73964fe48f73ba5 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 14:44:32 +0200 Subject: [PATCH 14/48] Return pending plugins in endpoint --- .../v2/resources/PendingPluginResource.java | 30 +++++++++- .../resources/PendingPluginResourceTest.java | 55 +++++++++++++++---- 2 files changed, 74 insertions(+), 11 deletions(-) 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 index c9d9982ccc..47e3d2f9f6 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -16,7 +17,9 @@ 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; @@ -50,6 +53,14 @@ public class PendingPluginResource { .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()); @@ -58,11 +69,28 @@ public class PendingPluginResource { } Embedded.Builder embedded = Embedded.embeddedBuilder(); - embedded.with("available", pending.stream().map(mapper::mapAvailable).collect(toList())); + 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({ 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 index 065ff9d723..d4b8a5bf5a 100644 --- 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 @@ -18,7 +18,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.PluginCondition; +import sonia.scm.plugin.InstalledPlugin; +import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; @@ -27,7 +28,6 @@ import javax.ws.rs.core.Response; import javax.ws.rs.ext.ExceptionMapper; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; -import java.util.Collections; import static java.net.URI.create; import static java.util.Collections.singletonList; @@ -76,6 +76,11 @@ class PendingPluginResourceTest { 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 @@ -94,7 +99,7 @@ class PendingPluginResourceTest { @Test void shouldGetEmptyPluginListsWithoutInstallLinkWhenNoPendingPluginsPresent() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("not-available-plugin"); + AvailablePlugin availablePlugin = createAvailablePlugin("not-pending-plugin"); when(availablePlugin.isPending()).thenReturn(false); when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); @@ -103,12 +108,12 @@ class PendingPluginResourceTest { assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); assertThat(response.getContentAsString()).contains("\"_links\":{\"self\":{\"href\":\"/v2/plugins/pending\"}}"); - assertThat(response.getContentAsString()).doesNotContain("not-available-plugin"); + assertThat(response.getContentAsString()).doesNotContain("not-pending-plugin"); } @Test - void shouldGetPendingAvailablePluginListsWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { - AvailablePlugin availablePlugin = createAvailablePlugin("available-plugin"); + void shouldGetPendingAvailablePluginListWithInstallLink() throws URISyntaxException, UnsupportedEncodingException { + AvailablePlugin availablePlugin = createAvailablePlugin("pending-available-plugin"); when(availablePlugin.isPending()).thenReturn(true); when(pluginManager.getAvailable()).thenReturn(singletonList(availablePlugin)); @@ -116,7 +121,24 @@ class PendingPluginResourceTest { dispatcher.invoke(request, response); assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); - assertThat(response.getContentAsString()).contains("\"_embedded\":{\"available\":[{\"name\":\"available-plugin\""); + 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()); } @@ -182,11 +204,24 @@ class PendingPluginResourceTest { } private AvailablePlugin createAvailablePlugin(PluginInformation pluginInformation) { - AvailablePluginDescriptor descriptor = new AvailablePluginDescriptor( - pluginInformation, new PluginCondition(), Collections.emptySet(), "https://download.hitchhiker.com", null - ); + 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; + } } From 938e52fda0636c73052bf9612ff819789bb6d173 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 15:33:15 +0200 Subject: [PATCH 15/48] Do not delete files that are not there --- .../sonia/scm/repository/spi/ModifyCommandRequest.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java index a108a73546..d61e17c785 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModifyCommandRequest.java @@ -122,10 +122,12 @@ public class ModifyCommandRequest implements Resetable, Validateable { } void cleanup() { - try { - IOUtil.delete(content); - } catch (IOException e) { - LOG.warn("could not delete temporary file {}", content, e); + if (content.exists()) { + try { + IOUtil.delete(content); + } catch (IOException e) { + LOG.warn("could not delete temporary file {}", content, e); + } } } } From fcba0999a7046f42742aa10b1544b4a657e23a1c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 15:48:48 +0000 Subject: [PATCH 16/48] Close branch feature/editor_file_create From 1bf49eab7bd2e86712ca1b2cad277310dd54dd80 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Fri, 13 Sep 2019 18:13:10 +0200 Subject: [PATCH 17/48] Add constructor with cause --- scm-core/src/main/java/sonia/scm/BadRequestException.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/BadRequestException.java b/scm-core/src/main/java/sonia/scm/BadRequestException.java index 544ed75a0b..3290e77521 100644 --- a/scm-core/src/main/java/sonia/scm/BadRequestException.java +++ b/scm-core/src/main/java/sonia/scm/BadRequestException.java @@ -6,4 +6,8 @@ public abstract class BadRequestException extends ExceptionWithContext { public BadRequestException(List context, String message) { super(context, message); } + + public BadRequestException(List context, String message, Exception cause) { + super(context, message, cause); + } } From 01422587052af5ed356c7572e2869c3e0a47e58c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 09:55:38 +0200 Subject: [PATCH 18/48] Extract plugin creation for unit tests to util class --- .../InstalledPluginResourceTest.java | 17 ++------- .../api/v2/resources/PluginDtoMapperTest.java | 33 ++++------------- .../scm/plugin/DefaultPluginManagerTest.java | 31 +--------------- .../sonia/scm/plugin/PluginTestHelper.java | 37 +++++++++++++++++++ 4 files changed, 49 insertions(+), 69 deletions(-) create mode 100644 scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java 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 7fa0081c5c..8971ab5a8d 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 @@ -33,6 +33,7 @@ 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.*; +import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) class InstalledPluginResourceTest { @@ -85,7 +86,7 @@ class InstalledPluginResourceTest { @Test void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { - InstalledPlugin installedPlugin = createPlugin(); + InstalledPlugin installedPlugin = createInstalled(""); when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin))).thenReturn(new MockedResultDto()); @@ -104,7 +105,7 @@ class InstalledPluginResourceTest { PluginInformation pluginInformation = new PluginInformation(); pluginInformation.setVersion("2.0.0"); pluginInformation.setName("pluginName"); - InstalledPlugin installedPlugin = createPlugin(pluginInformation); + InstalledPlugin installedPlugin = createInstalled(pluginInformation); when(pluginManager.getInstalled("pluginName")).thenReturn(Optional.of(installedPlugin)); @@ -123,18 +124,6 @@ class InstalledPluginResourceTest { } } - private InstalledPlugin createPlugin() { - return createPlugin(new PluginInformation()); - } - - private InstalledPlugin createPlugin(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; - } - @Nested class WithoutAuthorization { 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 5cf6bdd45a..27e3ca32ea 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 @@ -21,6 +21,8 @@ import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static sonia.scm.plugin.PluginTestHelper.createAvailable; +import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) class PluginDtoMapperTest { @@ -72,22 +74,16 @@ class PluginDtoMapperTest { @Test void shouldAppendInstalledSelfLink() { - InstalledPlugin plugin = createInstalled(); + InstalledPlugin plugin = createInstalled(createPluginInformation()); PluginDto dto = mapper.mapInstalled(plugin); assertThat(dto.getLinks().getLinkBy("self").get().getHref()) .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin"); } - private InstalledPlugin createInstalled(PluginInformation information) { - InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); - when(plugin.getDescriptor().getInformation()).thenReturn(information); - return plugin; - } - @Test void shouldAppendAvailableSelfLink() { - AvailablePlugin plugin = createAvailable(); + AvailablePlugin plugin = createAvailable(createPluginInformation()); PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getLinks().getLinkBy("self").get().getHref()) @@ -96,7 +92,7 @@ class PluginDtoMapperTest { @Test void shouldNotAppendInstallLinkWithoutPermissions() { - AvailablePlugin plugin = createAvailable(); + AvailablePlugin plugin = createAvailable(createPluginInformation()); PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getLinks().getLinkBy("install")).isEmpty(); @@ -105,7 +101,7 @@ class PluginDtoMapperTest { @Test void shouldAppendInstallLink() { when(subject.isPermitted("plugin:manage")).thenReturn(true); - AvailablePlugin plugin = createAvailable(); + AvailablePlugin plugin = createAvailable(createPluginInformation()); PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getLinks().getLinkBy("install").get().getHref()) @@ -123,25 +119,10 @@ class PluginDtoMapperTest { @Test void shouldAppendDependencies() { - AvailablePlugin plugin = createAvailable(); + AvailablePlugin plugin = createAvailable(createPluginInformation()); when(plugin.getDescriptor().getDependencies()).thenReturn(ImmutableSet.of("one", "two")); PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getDependencies()).containsOnly("one", "two"); } - - private InstalledPlugin createInstalled() { - return createInstalled(createPluginInformation()); - } - - private AvailablePlugin createAvailable() { - return createAvailable(createPluginInformation()); - } - - private AvailablePlugin createAvailable(PluginInformation information) { - AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); - when(descriptor.getInformation()).thenReturn(information); - return new AvailablePlugin(descriptor); - } - } 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 322163ee1a..16e1e2d73a 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -26,6 +26,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.*; +import static sonia.scm.plugin.PluginTestHelper.createAvailable; +import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) class DefaultPluginManagerTest { @@ -354,33 +356,4 @@ class DefaultPluginManagerTest { } } - - private AvailablePlugin createAvailable(String name) { - PluginInformation information = new PluginInformation(); - information.setName(name); - return createAvailable(information); - } - - private InstalledPlugin createInstalled(String name) { - PluginInformation information = new PluginInformation(); - information.setName(name); - return createInstalled(information); - } - - private InstalledPlugin createInstalled(PluginInformation information) { - InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); - returnInformation(plugin, information); - return plugin; - } - - private AvailablePlugin createAvailable(PluginInformation information) { - AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); - lenient().when(descriptor.getInformation()).thenReturn(information); - return new AvailablePlugin(descriptor); - } - - private void returnInformation(Plugin mockedPlugin, PluginInformation information) { - when(mockedPlugin.getDescriptor().getInformation()).thenReturn(information); - } - } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java new file mode 100644 index 0000000000..7e3577d775 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java @@ -0,0 +1,37 @@ +package sonia.scm.plugin; + +import org.mockito.Answers; + +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class PluginTestHelper { + public static AvailablePlugin createAvailable(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createAvailable(information); + } + + public static InstalledPlugin createInstalled(String name) { + PluginInformation information = new PluginInformation(); + information.setName(name); + return createInstalled(information); + } + + public static InstalledPlugin createInstalled(PluginInformation information) { + InstalledPlugin plugin = mock(InstalledPlugin.class, Answers.RETURNS_DEEP_STUBS); + returnInformation(plugin, information); + return plugin; + } + + public static AvailablePlugin createAvailable(PluginInformation information) { + AvailablePluginDescriptor descriptor = mock(AvailablePluginDescriptor.class); + lenient().when(descriptor.getInformation()).thenReturn(information); + return new AvailablePlugin(descriptor); + } + + private static void returnInformation(Plugin mockedPlugin, PluginInformation information) { + when(mockedPlugin.getDescriptor().getInformation()).thenReturn(information); + } +} From 14451897b2bdfca21e82b1a2bb31852811b4340a Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 11:42:26 +0200 Subject: [PATCH 19/48] Introduce PluginDependencyTracker --- .../scm/plugin/PluginDependencyTracker.java | 42 ++++++++++ .../plugin/PluginDependencyTrackerTest.java | 81 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java create mode 100644 scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java new file mode 100644 index 0000000000..bc5cd1dc6d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginDependencyTracker.java @@ -0,0 +1,42 @@ +package sonia.scm.plugin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import static sonia.scm.ScmConstraintViolationException.Builder.doThrow; + +class PluginDependencyTracker { + + private final Map> plugins = new HashMap<>(); + + void addInstalled(PluginDescriptor plugin) { + plugin.getDependencies().forEach(dependency -> addDependency(plugin.getInformation().getName(), dependency)); + } + + void removeInstalled(PluginDescriptor plugin) { + doThrow() + .violation("Plugin is needed as a dependency for other plugins", "plugin") + .when(!mayUninstall(plugin.getInformation().getName())); + plugin.getDependencies().forEach(dependency -> removeDependency(plugin.getInformation().getName(), dependency)); + } + + boolean mayUninstall(String name) { + return plugins.computeIfAbsent(name, x -> new HashSet<>()).isEmpty(); + } + + private void addDependency(String from, String to) { + plugins.computeIfAbsent(to, name -> createInitialDependencyCollection(from)); + } + + private void removeDependency(String from, String to) { + plugins.get(to).remove(from); + } + + private Collection createInitialDependencyCollection(String from) { + Collection dependencies = new HashSet<>(); + dependencies.add(from); + return dependencies; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java new file mode 100644 index 0000000000..c38409eb51 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginDependencyTrackerTest.java @@ -0,0 +1,81 @@ +package sonia.scm.plugin; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import sonia.scm.ScmConstraintViolationException; + +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static sonia.scm.plugin.PluginTestHelper.createInstalled; + +class PluginDependencyTrackerTest { + + @Test + void simpleInstalledPluginWithoutDependingPluginsCanBeUninstalled() { + PluginDescriptor mail = createInstalled("scm-mail-plugin").getDescriptor(); + when(mail.getDependencies()).thenReturn(emptySet()); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(mail); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isTrue(); + } + + @Test + void installedPluginWithDependingPluginCannotBeUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isFalse(); + } + + @Test + void uninstallOfRequiredPluginShouldThrowException() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + + Assertions.assertThrows( + ScmConstraintViolationException.class, + () -> pluginDependencyTracker.removeInstalled(createInstalled("scm-mail-plugin").getDescriptor()) + ); + } + + @Test + void installedPluginWithDependingPluginCanBeUninstalledAfterDependingPluginIsUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + pluginDependencyTracker.removeInstalled(review); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isTrue(); + } + + @Test + void installedPluginWithMultipleDependingPluginCannotBeUninstalledAfterOnlyOneDependingPluginIsUninstalled() { + PluginDescriptor review = createInstalled("scm-review-plugin").getDescriptor(); + when(review.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + PluginDescriptor jira = createInstalled("scm-jira-plugin").getDescriptor(); + when(jira.getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + PluginDependencyTracker pluginDependencyTracker = new PluginDependencyTracker(); + pluginDependencyTracker.addInstalled(review); + pluginDependencyTracker.addInstalled(jira); + pluginDependencyTracker.removeInstalled(jira); + + boolean mayUninstall = pluginDependencyTracker.mayUninstall("scm-mail-plugin"); + assertThat(mayUninstall).isFalse(); + } +} From 88ed3ff023418efc8933c3c87dc021b5938c584f Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 13:22:26 +0200 Subject: [PATCH 20/48] Add uninstall method to plugin manager --- .../java/sonia/scm/plugin/PluginManager.java | 8 +++ .../scm/plugin/DefaultPluginManager.java | 37 ++++++++++++-- .../scm/plugin/DefaultPluginManagerTest.java | 50 +++++++++++++++++-- 3 files changed, 88 insertions(+), 7 deletions(-) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java index b7b8f69519..f84a975452 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -81,6 +81,14 @@ public interface PluginManager { */ void install(String name, boolean restartAfterInstallation); + /** + * Marks the plugin with the given name for uninstall. + * + * @param name plugin name + * @param restartAfterInstallation restart context after plugin has been marked to really uninstall the plugin + */ + void uninstall(String name, boolean restartAfterInstallation); + /** * Install all pending plugins and restart the scm context. */ 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 807eaac317..11e42891e4 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -33,8 +33,7 @@ package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- - +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -43,9 +42,11 @@ import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; -//~--- JDK imports ------------------------------------------------------------ import javax.inject.Inject; +import java.io.IOException; +import java.nio.file.Files; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Optional; import java.util.Set; @@ -54,6 +55,8 @@ import java.util.stream.Collectors; import static sonia.scm.ContextEntry.ContextBuilder.entity; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -67,7 +70,8 @@ public class DefaultPluginManager implements PluginManager { private final PluginLoader loader; private final PluginCenter center; private final PluginInstaller installer; - private final List pendingQueue = new ArrayList<>(); + private final Collection pendingQueue = new ArrayList<>(); + private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) { @@ -75,6 +79,16 @@ public class DefaultPluginManager implements PluginManager { this.loader = loader; this.center = center; this.installer = installer; + + this.computeRequiredPlugins(); + } + + @VisibleForTesting + synchronized void computeRequiredPlugins() { + loader.getInstalledPlugins() + .stream() + .map(InstalledPlugin::getDescriptor) + .forEach(dependencyTracker::addInstalled); } @Override @@ -153,6 +167,21 @@ public class DefaultPluginManager implements PluginManager { } } + @Override + public void uninstall(String name, boolean restartAfterInstallation) { + PluginPermissions.manage().check(); + InstalledPlugin installed = getInstalled(name) + .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); + + dependencyTracker.removeInstalled(installed.getDescriptor()); + + try { + Files.createFile(installed.getDirectory().resolve("uninstall")); + } catch (IOException e) { + throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); + } + } + @Override public void installPendingAndRestart() { PluginPermissions.manage().check(); 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 16e1e2d73a..44799a03fb 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -10,26 +10,37 @@ 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.Answers; +import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; +import sonia.scm.ScmConstraintViolationException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import java.nio.file.Path; import java.util.List; import java.util.Optional; +import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.in; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.doReturn; +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; import static sonia.scm.plugin.PluginTestHelper.createAvailable; import static sonia.scm.plugin.PluginTestHelper.createInstalled; @ExtendWith(MockitoExtension.class) +@ExtendWith(TempDirectory.class) class DefaultPluginManagerTest { @Mock @@ -305,6 +316,34 @@ class DefaultPluginManagerTest { assertThat(available.get(0).isPending()).isTrue(); } + @Test + void shouldThrowExceptionWhenUninstallingUnknownPlugin() { + assertThrows(NotFoundException.class, () -> manager.uninstall("no-such-plugin", false)); + } + + @Test + void shouldUseDependencyTrackerForUninstall() { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + InstalledPlugin reviewPlugin = createInstalled("scm-review-plugin"); + when(reviewPlugin.getDescriptor().getDependencies()).thenReturn(singleton("scm-mail-plugin")); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(mailPlugin, reviewPlugin)); + manager.computeRequiredPlugins(); + + assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); + } + + @Test + void shouldCreateUninstallFile(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + manager.uninstall("scm-mail-plugin", false); + + assertThat(temp.resolve("uninstall")).exists(); + } } @Nested @@ -350,6 +389,11 @@ class DefaultPluginManagerTest { assertThrows(AuthorizationException.class, () -> manager.install("test", false)); } + @Test + void shouldThrowAuthorizationExceptionsForUninstallMethod() { + assertThrows(AuthorizationException.class, () -> manager.uninstall("test", false)); + } + @Test void shouldThrowAuthorizationExceptionsForInstallPendingAndRestart() { assertThrows(AuthorizationException.class, () -> manager.installPendingAndRestart()); From 9ce3a6c00bd00e5007d6fe12c304ca4d631b04d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81ngel=20L=2E=20Garc=C3=ADa?= Date: Mon, 16 Sep 2019 13:31:58 +0200 Subject: [PATCH 21/48] Spanish files for i18n. --- scm-ui/public/locales/es/admin.json | 83 ++++++++ scm-ui/public/locales/es/commons.json | 90 +++++++++ scm-ui/public/locales/es/config.json | 78 ++++++++ scm-ui/public/locales/es/groups.json | 76 +++++++ scm-ui/public/locales/es/permissions.json | 6 + scm-ui/public/locales/es/repos.json | 189 ++++++++++++++++++ scm-ui/public/locales/es/users.json | 65 ++++++ .../main/resources/locales/es/plugins.json | 186 +++++++++++++++++ 8 files changed, 773 insertions(+) create mode 100644 scm-ui/public/locales/es/admin.json create mode 100644 scm-ui/public/locales/es/commons.json create mode 100644 scm-ui/public/locales/es/config.json create mode 100644 scm-ui/public/locales/es/groups.json create mode 100644 scm-ui/public/locales/es/permissions.json create mode 100644 scm-ui/public/locales/es/repos.json create mode 100644 scm-ui/public/locales/es/users.json create mode 100644 scm-webapp/src/main/resources/locales/es/plugins.json diff --git a/scm-ui/public/locales/es/admin.json b/scm-ui/public/locales/es/admin.json new file mode 100644 index 0000000000..14abf626d6 --- /dev/null +++ b/scm-ui/public/locales/es/admin.json @@ -0,0 +1,83 @@ +{ + "admin": { + "menu": { + "navigationLabel": "Menú de administración", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General" + }, + "info": { + "currentAppVersion": "Versión actual de la aplicación", + "communityTitle": "Soporte de la comunidad", + "communityIconAlt": "Icono del soporte de la comunidad", + "communityInfo": "Contacte con el equipo de soporte de SCM-Manager para questiones acerca de SCM-Manager, para informar de errores o pedir nuevas funcionalidades use los canales oficiales.", + "communityButton": "Contactar con nuestro equipo", + "enterpriseTitle": "Soporte empresarial", + "enterpriseIconAlt": "Icono del soporte para empresas", + "enterpriseInfo": "¿Necesita ayuda para la integración de SMC-Manager en sus procesos, con la personalización de la herramienta o simplemente un acuerdo de nivel de servicio (SLA)?", + "enterprisePartner": "Póngase en contacto con nuestro socio de desarrollo Cloudogu! Su equipo está esperando para tratar sus requisitos con usted y estará encantado de darle un presupuesto.", + "enterpriseLink": "https://cloudogu.com/en/scm-manager-enterprise/", + "enterpriseButton": "Pedir soporte empresarial" + } + }, + "plugins": { + "title": "Complementos", + "installedSubtitle": "Complementos instalados", + "availableSubtitle": "Complementos disponibles", + "menu": { + "pluginsNavLink": "Complementos", + "installedNavLink": "Instalados", + "availableNavLink": "Disponibles" + }, + "installPending": "Instalar los complementos pendientes", + "noPlugins": "No se han encontrado complementos.", + "modal": { + "title": "Instalar complemento {{name}} ", + "restart": "Reiniciar para activar", + "install": "Instalar", + "installAndRestart": "Instalar y reiniciar", + "abort": "Cancelar", + "author": "Autor", + "version": "Versión", + "dependencyNotification": "Con este complemento las siguientes dependencias serán instaladas si no lo han sido ya", + "dependencies": "Dependencias", + "successNotification": "Complemento instalado correctamente. Necesita recargar la página para ver los cambios:", + "reload": "recargar ahora", + "restartNotification": "Usted debería reiniciar scm-manager sólo si actualmente no hay nadie trabajando con el.", + "installPending": "Los siguientes complementos serán instalados y después de la instalación sdm-manager será reiniciado." + } + }, + "repositoryRole": { + "navLink": "Roles y permisos", + "title": "Roles y permisos", + "errorTitle": "Error", + "errorSubtitle": "Error desconocido", + "createSubtitle": "Crear nuevo rol", + "editSubtitle": "Editar rol", + "overview": { + "title": "Visión general de todos los roles", + "noPermissionRoles": "No se han encontrado roles.", + "createButton": "Crear rol" + }, + "editButton": "Editar", + "name": "Nombre", + "type": "Tipo", + "verbs": "Permisos", + "system": "Sistema", + "form": { + "name": "Nombre", + "permissions": "Permisos", + "submit": "Guardar" + }, + "delete": { + "button": "Borrar", + "subtitle": "Eliminar el rol", + "confirmAlert": { + "title": "Eliminar el rol", + "message": "¿Realmente desea borrar el rol? Todos los usuarios de este rol perderń sus permisos.", + "submit": "Sí", + "cancel": "No" + } + } + } +} diff --git a/scm-ui/public/locales/es/commons.json b/scm-ui/public/locales/es/commons.json new file mode 100644 index 0000000000..e130ce8b17 --- /dev/null +++ b/scm-ui/public/locales/es/commons.json @@ -0,0 +1,90 @@ +{ + "login": { + "title": "Iniciar sesión", + "subtitle": "Por favor inicie sesión para continuar", + "logo-alt": "SCM-Manager", + "username-placeholder": "Su nombre de usuario", + "password-placeholder": "Su contraseña", + "submit": "Iniciar sesión", + "plugin": "Plugin", + "feature": "Feature", + "tip": "Tip", + "loading": "Cargando ...", + "error": "Error" + }, + "logout": { + "error": { + "title": "Cierre de sesión fallido", + "subtitle": "Ha ocurrido un error al cerrar la sesión" + } + }, + "app": { + "error": { + "title": "Error", + "subtitle": "Ha ocurrido un error desconocido" + } + }, + "errorNotification": { + "prefix": "Error", + "loginLink": "Aquí puede iniciar la sesión de nuevo.", + "timeout": "La sesión ha caducado", + "wrongLoginCredentials": "Credenciales incorrectas", + "forbidden": "Usted no tiene permiso para ver esta sección" + }, + "loading": { + "alt": "Cargando ..." + }, + "logo": { + "alt": "SCM-Manager" + }, + "primary-navigation": { + "repositories": "Repositorios", + "users": "Usuarios", + "logout": "Cerrar sesión", + "groups": "Grupos", + "admin": "Administración" + }, + "filterEntries": "Filtrar entradas", + "autocomplete": { + "group": "Grupo", + "user": "Usuario", + "noGroupOptions": "No hay sugerencias disponibles", + "groupPlaceholder": "Nombre del grupo", + "noUserOptions": "No hay sugerencias disponibles", + "userPlaceholder": "Nombre de usuario", + "loading": "Cargando..." + }, + "paginator": { + "next": "Siguiente", + "previous": "Anterior" + }, + "profile": { + "navigationLabel": "Menú de sección", + "informationNavLink": "Información", + "changePasswordNavLink": "Cambiar contraseña", + "settingsNavLink": "Ajustes", + "username": "Nombre de usuario", + "displayName": "Nombre a mostrar", + "mail": "Correo electrónico", + "groups": "Grupos", + "information": "Información", + "change-password": "Cambiar contraseña", + "error-title": "Error", + "error-subtitle": "No se puede mostrar la sección", + "error": "Error", + "error-message": "'me' no está definido" + }, + "password": { + "label": "Contraseña", + "newPassword": "Nueva contraseña", + "passwordHelpText": "Contraseña del usuario en texto plano", + "passwordConfirmHelpText": "Repita la contraseña para confirmar", + "currentPassword": "Contraseña actual", + "currentPasswordHelpText": "La contraseña ya está en uso", + "confirmPassword": "Confirme la contraseña", + "passwordInvalid": "La contraseña debe tener entre 6 y 32 caracteres", + "passwordConfirmFailed": "Las contraseñas deben ser identicas", + "submit": "Guardar", + "changedSuccessfully": "Contraseña cambiada correctamente" + } +} diff --git a/scm-ui/public/locales/es/config.json b/scm-ui/public/locales/es/config.json new file mode 100644 index 0000000000..3b5ab843dd --- /dev/null +++ b/scm-ui/public/locales/es/config.json @@ -0,0 +1,78 @@ +{ + "config": { + "navigationLabel": "Menú de administración", + "title": "Configuración global", + "errorTitle": "Error", + "errorSubtitle": "Error de configuración desconocido", + "form": { + "submit": "Enviar", + "submit-success-notification": "¡Configuración cambiada correctamente!", + "no-read-permission-notification": "Por favor, tenga en cuenta: ¡No tiene permiso para ver la configuración!", + "no-write-permission-notification": "Por favor, tenga en cuenta: ¡No tiene permiso para editar la configuración!" + } + }, + "proxy-settings": { + "name": "Ajustes del proxy", + "proxy-password": "Contraseña del proxy", + "proxy-port": "Puerto del proxy", + "proxy-server": "Servidor proxy", + "proxy-user": "Usuario del proxy", + "enable-proxy": "Habilitar proxy", + "proxy-excludes": "Excepciones del proxy", + "remove-proxy-exclude-button": "Eliminar las excepciones del proxy", + "add-proxy-exclude-error": "La excepción que desea añadir al proxy es incorrecta", + "add-proxy-exclude-textfield": "Añada aquí las excepciones que desee incluir al proxy", + "add-proxy-exclude-button": "Añadir excepción al proxy" + }, + "base-url-settings": { + "name": "Ajustes de la URL base", + "base-url": "URL base", + "force-base-url": "Forzar la URL base" + }, + "login-attempt": { + "name": "Intento de inicio de sesión", + "login-attempt-limit": "Límite de intentos de inicio de sesión", + "login-attempt-limit-timeout": "Tiempo de espera para el intento de inicio de sesión" + }, + "general-settings": { + "realm-description": "Descripción del dominio", + "disable-grouping-grid": "Deshabilitar grupos", + "date-format": "Formato de la fecha", + "anonymous-access-enabled": "Acceso anónimo habilitado", + "skip-failed-authenticators": "Omitir autenticadores fallidos", + "plugin-url": "URL del almacén de complementos", + "enabled-xsrf-protection": "Protección XSRF habilitada", + "namespace-strategy": "Estrategia para el espacio de nombres", + "login-info-url": "URL de información de inicio de sesión" + }, + "validation": { + "date-format-invalid": "El formato de la fecha es incorrecto", + "login-attempt-limit-timeout-invalid": "El valor no es un número", + "login-attempt-limit-invalid": "El valor no es un número", + "plugin-url-invalid": "La URL es incorrecta" + }, + "help": { + "realmDescriptionHelpText": "Descripción del dominio de autenticación.", + "dateFormatHelpText": "Formato de la fecha. Por favor, heche un vistazo a la documentación de MomentJS.", + "pluginUrlHelpText": "La URL de la API del almacén de complementos. Explicación de los marcadores: version = Versión de SCM-Manager; os = Sistema operativo; arch = Arquitectura", + "enableForwardingHelpText": "Habilitar el redireccionamiento de puertos para mod_proxy.", + "disableGroupingGridHelpText": "Deshabilitar los grupos de repositorios. Se requiere una recarga completa de la página después de un cambio en este valor.", + "allowAnonymousAccessHelpText": "Los usuarios anónimos tienen acceso de lectura en los repositorios públicos.", + "skipFailedAuthenticatorsHelpText": "No detenga la cadena de autenticación si un autenticador encuentra al usuario pero no puede autenticarlo.", + "adminGroupsHelpText": "Nombres de los grupos con permisos de administrador.", + "adminUsersHelpText": "Nombres de los usuarios con permisos de administrador.", + "forceBaseUrlHelpText": "Redirige a la URL base si la solicitud proviene de otra URL.", + "baseUrlHelpText": "La URL de la aplicación (con la ruta del contexto), por ejemplo: http://localhost:8080/scm", + "loginAttemptLimitHelpText": "Máximo número permitido de intentos de inicio de sesión. Use -1 para deshabilitar este límite.", + "loginAttemptLimitTimeoutHelpText": "Tiempo de espera en segundos para los usuarios que están deshabilitados temporalmente debido a demasiado intentos fallidos de inicio de sesión.", + "enableProxyHelpText": "Habilitar proxy", + "proxyPortHelpText": "El puerto del proxy", + "proxyPasswordHelpText": "La contraseña para la autenticación del servidor proxy.", + "proxyServerHelpText": "El servidor proxy", + "proxyUserHelpText": "El nombre de usuario para la autenticación del servidor proxy.", + "proxyExcludesHelpText": "Patrones globales para hostnames que deben excluirse de la configuración del proxy.", + "enableXsrfProtectionHelpText": "Habilitar la protección de cookies XSRF. Nota: Esta funcionalidad todavía es experimental.", + "nameSpaceStrategyHelpText": "La estrategia para el espacio de nombres.", + "loginInfoUrlHelpText": "URL para la información en el inicio de sesión (consejos sobre complementos y funcionalidades en la página de inicio de sesión). Si esto se omite, no se mostrará información de inicio de sesión." + } +} diff --git a/scm-ui/public/locales/es/groups.json b/scm-ui/public/locales/es/groups.json new file mode 100644 index 0000000000..ffba16c354 --- /dev/null +++ b/scm-ui/public/locales/es/groups.json @@ -0,0 +1,76 @@ +{ + "group": { + "name": "Nombre", + "description": "Descripción", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación", + "type": "Tipo", + "external": "Externo", + "internal": "Interno", + "members": "Miembros" + }, + "groups": { + "title": "Grupos", + "subtitle": "Crear, leer, actualizar y borrar grupos", + "noGroups": "No se han encontrado grupos." + }, + "singleGroup": { + "errorTitle": "Error", + "errorSubtitle": "Error de grupo desconocido", + "menu": { + "navigationLabel": "Menú de grupo", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "setPermissionsNavLink": "Permisos" + } + }, + "add-group": { + "title": "Crear grupo", + "subtitle": "Crear un nuevo grupo" + }, + "create-group-button": { + "label": "Crear grupo" + }, + "edit-group-button": { + "label": "Editar" + }, + "add-member-button": { + "label": "Añadir miembro" + }, + "remove-member-button": { + "label": "Eliminar miembro" + }, + "add-member-textfield": { + "label": "Añadir miembro", + "error": "El nombre del miembro es incorrecto" + }, + "add-member-autocomplete": { + "placeholder": "Introducir el nombre del miembro", + "loading": "Cargando...", + "no-options": "No hay sugerencias disponibles" + }, + "groupForm": { + "subtitle": "Editar grupo", + "externalSubtitle": "Editar grupo externo", + "submit": "Guardar", + "nameError": "El nombre del grupo es incorrecto", + "descriptionError": "La descripción es incorrecta", + "help": { + "nameHelpText": "Nombre único del grupo", + "descriptionHelpText": "Descripción breve del grupo", + "memberHelpText": "Nombres de usuario de los miembros del grupo", + "externalHelpText": "Los miembros son gestionados por un sistema externo como por ejemplo LDAP" + } + }, + "deleteGroup": { + "subtitle": "Borrar grupo", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar grupo", + "message": "¿Realmente desea borrar el grupo?", + "submit": "Sí", + "cancel": "No" + } + } +} diff --git a/scm-ui/public/locales/es/permissions.json b/scm-ui/public/locales/es/permissions.json new file mode 100644 index 0000000000..d3da1103ea --- /dev/null +++ b/scm-ui/public/locales/es/permissions.json @@ -0,0 +1,6 @@ +{ + "setPermissions": { + "button": "Guardar", + "setPermissionsSuccessful": "Permisos guardados correctamente" + } +} diff --git a/scm-ui/public/locales/es/repos.json b/scm-ui/public/locales/es/repos.json new file mode 100644 index 0000000000..08c9235593 --- /dev/null +++ b/scm-ui/public/locales/es/repos.json @@ -0,0 +1,189 @@ +{ + "repository": { + "namespace": "Espacio de nombres", + "name": "Nombre", + "type": "Tipo", + "contact": "Contacto", + "description": "Descripción", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación" + }, + "validation": { + "namespace-invalid": "El espacio de nombres del repositorio es incorrecto", + "name-invalid": "El nombre del repositorio es incorrecto", + "contact-invalid": "El contacto debe ser una dirección de correo electrónico válida", + "branch": { + "nameInvalid": "El nombre de la rama es incorrecto" + } + }, + "help": { + "namespaceHelpText": "El espacio de nombres del repositorio. Este nombre formará parte de la URL del repositorio.", + "nameHelpText": "El nombre del repositorio. Este nombre formará parte de la URL del repositorio.", + "typeHelpText": "El tipo del repositorio (Mercurial, Git or Subversion).", + "contactHelpText": "Dirección del correo electrónico de la persona responsable del repositorio.", + "descriptionHelpText": "Breve descripción del repositorio." + }, + "repositoryRoot": { + "errorTitle": "Error", + "errorSubtitle": "Error de repositorio desconocido", + "menu": { + "navigationLabel": "Menú de repositorio", + "informationNavLink": "Información", + "branchesNavLink": "Ramas", + "historyNavLink": "Commits", + "sourcesNavLink": "Fuentes", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "permissionsNavLink": "Permisos" + } + }, + "overview": { + "title": "Repositorios", + "subtitle": "Visión general de los repositorios disponibles", + "noRepositories": "No se han encontrado repositorios.", + "createButton": "Crear repositorio" + }, + "create": { + "title": "Crear repositorio", + "subtitle": "Crear un nuevo repositorio" + }, + "branches": { + "overview": { + "title": "Vivisón general de todas las ramas", + "noBranches": "No se han encontrado ramas.", + "createButton": "Crear rama" + }, + "table": { + "branches": "Ramas" + }, + "create": { + "title": "Crear rama", + "source": "Rama padre", + "name": "Nombre", + "submit": "Crear rama" + } + }, + "branch": { + "name": "Nombre:", + "commits": "Commits", + "sources": "Fuentes", + "defaultTag": "Por defecto" + }, + "changesets": { + "errorTitle": "Error", + "errorSubtitle": "No se han podido recuperar los changesets", + "noChangesets": "No se han encontrado changesets para esta rama branch.", + "branchSelectorLabel": "Ramas" + }, + "changeset": { + "description": "Descripción", + "summary": "El changeset {{id}} fue entregado {{time}}", + "shortSummary": "Entregado {{id}} {{time}}", + "tags": "Etiquetas", + "diffNotSupported": "La comparación de changesets no es soportada por el tipo de repositorio", + "author": { + "prefix": "Creado por", + "mailto": "Enviar correo electrónico a" + }, + "buttons": { + "details": "Detalles", + "sources": "Fuentes" + } + }, + "repositoryForm": { + "subtitle": "Editar repositorio", + "submit": "Guardar" + }, + "sources": { + "file-tree": { + "name": "Nombre", + "length": "Longitud", + "lastModified": "Última modificación", + "description": "Descripción", + "branch": "Rama" + }, + "content": { + "historyButton": "Historia", + "sourcesButton": "Fuentes", + "downloadButton": "Descargar", + "path": "Ruta", + "branch": "Rama", + "lastModified": "Última modificación", + "description": "Discripción", + "size": "tamaño" + }, + "noSources": "No se han encontrado fuentes para esta rama." + }, + "permission": { + "title": "Editar permisos", + "user": "Usuario", + "group": "Grupo", + "error-title": "Error", + "error-subtitle": "Error de permisos desconocido", + "name": "Usuario o grupo", + "role": "Rol", + "custom": "Personalizar", + "permissions": "Permisos", + "group-permission": "Permiso de grupo", + "user-permission": "Permiso de usuario", + "edit-permission": { + "delete-button": "Borrar", + "save-button": "Guardar cambios" + }, + "advanced-button": { + "label": "Avanzado" + }, + "delete-permission-button": { + "label": "Borrar", + "confirm-alert": { + "title": "Borrar permiso", + "message": "¿Realmente desea borrar el permiso?", + "submit": "Sí", + "cancel": "No" + } + }, + "add-permission": { + "add-permission-heading": "Añadir nuevo permiso", + "submit-button": "Guardar", + "name-input-invalid": "¡No se permiten permisos vacíos! ¡Si el permiso no está vacío, su nombre es inválido o ya existe!" + }, + "help": { + "groupPermissionHelpText": "Establece si un permiso es de grupo. Si no está marcado es un permiso de usuario.", + "nameHelpText": "Gestionar los permisos de un usuario o grupo.", + "roleHelpText": "READ = leer; WRITE = leer and escribir; OWNER = leer, escribir y también la capacidad de gestionar las propiedades y permisos. Si no hay nada seleccionado use el botón 'Avanzado' para ver los permisos en detalle.", + "permissionsHelpText": "Use esto para especificar su propio conjunto de permisos independientemente de los roles predefinidos." + }, + "advanced": { + "dialog": { + "title": "Permisos avanzados", + "submit": "Guardar", + "abort": "Cancelar" + } + } + }, + "deleteRepo": { + "subtitle": "Borrar repositorio", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar repositorio", + "message": "¿Realmente desea borrar el repositorio?", + "submit": "sí", + "cancel": "No" + } + }, + "diff": { + "changes": { + "add": "añadido", + "delete": "borrado", + "modify": "modificado", + "rename": "renombrado", + "copy": "copiado" + }, + "sideBySide": "dos columnas", + "combined": "combinado" + }, + "fileUpload": { + "clickHere": "Haga click aquí para seleccionar su fichero", + "dragAndDrop": "Arrastre y suelte los ficheros aquí" + } +} diff --git a/scm-ui/public/locales/es/users.json b/scm-ui/public/locales/es/users.json new file mode 100644 index 0000000000..d5ea97b95a --- /dev/null +++ b/scm-ui/public/locales/es/users.json @@ -0,0 +1,65 @@ +{ + "user": { + "name": "Nombre de usuario", + "displayName": "Nombre a mostrar", + "mail": "Correo electrónico", + "password": "Contraseña", + "active": "Activo", + "inactive": "Inactivo", + "type": "Tipo", + "creationDate": "Fecha de creación", + "lastModified": "Última modificación" + }, + "validation": { + "mail-invalid": "El correo electrónico es incorrecto", + "name-invalid": "El nombre es incorrecto", + "displayname-invalid": "El nombre a mostrar es incorrecto" + }, + "help": { + "usernameHelpText": "Nombre único del usuario.", + "displayNameHelpText": "Nombre de usuario a mostrar.", + "mailHelpText": "Dirección de correo electrónico del usuario.", + "adminHelpText": "Un administrador es capaz de crear, modificar y borrar repositorios, grupos y usuarios.", + "activeHelpText": "Activar o desactivar el usuario." + }, + "users": { + "title": "Usuarios", + "subtitle": "Crear, leer, actualizar y borrar usuarios", + "noUsers": "No se han encontrado usuarios.", + "createButton": "Crear usuario" + }, + "singleUser": { + "errorTitle": "Error", + "errorSubtitle": "Error de usuario desconocido", + "menu": { + "navigationLabel": "Menú de usuario", + "informationNavLink": "Información", + "settingsNavLink": "Ajustes", + "generalNavLink": "General", + "setPasswordNavLink": "Contraseña", + "setPermissionsNavLink": "Permisos" + } + }, + "createUser": { + "title": "Crear usuario", + "subtitle": "Crear un nuevo usuario" + }, + "deleteUser": { + "subtitle": "Borrar usuario", + "button": "Borrar", + "confirmAlert": { + "title": "Borrar usuario", + "message": "¿Realmente desea borrar el usuario?", + "submit": "Sí", + "cancel": "No" + } + }, + "singleUserPassword": { + "button": "Guardar", + "setPasswordSuccessful": "Contraseña guardada correctamente" + }, + "userForm": { + "subtitle": "Editar usuario", + "button": "Guardar" + } +} diff --git a/scm-webapp/src/main/resources/locales/es/plugins.json b/scm-webapp/src/main/resources/locales/es/plugins.json new file mode 100644 index 0000000000..cef33f6007 --- /dev/null +++ b/scm-webapp/src/main/resources/locales/es/plugins.json @@ -0,0 +1,186 @@ +{ + "permissions": { + "*": { + "displayName": "Administrador global", + "description": "Puede administrar la instancia completa" + }, + "repository": { + "read,pull": { + "*": { + "displayName": "Leer todos los repositorios", + "description": "Puede ver y clonar todos los repositorios" + } + }, + "read,pull,push": { + "*": { + "displayName": "Escribir todos los repositorios", + "description": "Puede ver, clonar y enviar cambios a todos los repositorios" + } + }, + "*": { + "displayName": "Poseer todos los repositorios", + "description": "Puede ver, clonar, enviar cambios, configurar y eliminar todos los repositorios" + }, + "create": { + "displayName": "Crear repositorios", + "description": "Puede crear repositorios" + } + }, + "user": { + "*": { + "displayName": "Administrar usuarios", + "description": "Puede administrar todos los usuarios" + } + }, + "group": { + "*": { + "displayName": "Administrar grupos", + "description": "Puede administrar todos los grupos" + } + }, + "permission": { + "*": { + "displayName": "Administrar permisos", + "description": "Puede administrar permisos (addicionalmente necesita 'administrar usuarios' y/o 'administrar grupos')." + } + }, + "configuration": { + "list": { + "displayName": "Administración basíca", + "description": "Prerequisito para todos los permisos administrativos. Sin este permiso la configuración no será visible." + }, + "read,write": { + "global": { + "displayName": "Administrar núcleo", + "description": "Puede administrar las opciones centrales de SCM-Manager" + }, + "*": { + "displayName": "Administrar núcleo y complementos", + "description": "Puede configurar las opciones centrales de SCM-Manager y todos los complementos (plugins)" + } + } + }, + "repositoryRole": { + "read,write": { + "displayName": "Administrar permisos de roles de repositorio personalizados", + "description": "Puede crear, modificar y borrar roles de repositorios personalizados y sus permisos" + } + }, + "plugin": { + "read": { + "displayName": "Leer todos los complementos", + "description": "Puede leer todos los complementos instalados y los disponibles" + }, + "read,write": { + "displayName": "Leer y gestionar todos los complementos", + "description": "Puede leer y gestionar todos los complementos instalados y los diponibles" + } + }, + "unknown": "Permiso desconocido" + }, + "verbs": { + "repository": { + "read": { + "displayName": "leer repositorio", + "description": "Puede leer el repositorio dentro de SCM-Manager" + }, + "modify": { + "displayName": "modificar los metadatos del repositorio", + "description": "Puede modificar las propiedades básicas del repositorio" + }, + "delete": { + "displayName": "borrar repositorio", + "description": "Puede borrar el repositorio" + }, + "pull": { + "displayName": "Traer los cambios o crear copia local del repositorio (pull/checkout)", + "description": "Puede traer los cambios o crear una copia de trabajo local desde el repositorio (pull/checkout)" + }, + "push": { + "displayName": "Publicar o enviar cambios al repositorio (push/commit)", + "description": "Puede cambiar el contenido del repositorio (push/commit)" + }, + "permissionRead": { + "displayName": "leer permisos", + "description": "Puede ver los permisos del repositorio" + }, + "permissionWrite": { + "displayName": "modificar permisos", + "description": "Puede modificar los permisos del repositorio" + }, + "*": { + "displayName": "poseer repositorio", + "description": "Puede cambiar todo en el repositorio (incluidos los demás permisos)" + } + } + }, + "errors": { + "context": "Contexto", + "errorCode": "Código de error", + "transactionId": "Identificador de la transacción", + "moreInfo": "Para más información ver", + "violations": "Violaciones:", + "AGR7UzkhA1": { + "displayName": "No encontrado", + "description": "No se pudo encontrar la entidad solicitada. Puede haber sido eliminado en otra sesión" + }, + "FtR7UznKU1": { + "displayName": "Ya existe", + "description": "Ya hay una entidad con el mismo valor de clave." + }, + "9BR7qpDAe1": { + "displayName": "No se permite cambiar la contraseña", + "description": "No tiene permiso para cambiar la contraseña." + }, + "2wR7UzpPG1": { + "displayName": "Modificaciones actuales", + "description": "La entidad ha sido modificada concurrentemente por otro usuario o proceso. Por favor recarge la entidad." + }, + "9SR8G0kmU1": { + "displayName": "Funcionalidad no soportada", + "description": "El sistema de control de versiones de este repositorio no admite la funcionalidad solicitada." + }, + "CmR8GCJb31": { + "displayName": "Error interno del servidor", + "description": "Se ha producido un error interno en el servidor. Por favor contacte con su administrador para obtener más ayuda." + }, + "92RCCCMHO1": { + "displayName": "No se ha podido encontrar la URL interna", + "description": "Una petición interna no ha podido ser manejada por el servidor. Por favor contacte con su administrador para obtener más ayuda." + }, + "2VRCrvpL71": { + "displayName": "Formato de datos invorrecto", + "description": "Los datos enviados al servidor son son correctos. Por favor revise los datos enviados o contacte con su administrador para obtener más ayuda." + }, + "8pRBYDURx1": { + "displayName": "Tipo de datos incorrecto", + "description": "El tipo de los datos enviados el servidor es incorrecto. Por favor contacte con su administrador para obtener más ayuda." + }, + "1wR7ZBe7H1": { + "displayName": "Entrada incorrecta", + "description": "Los datos no pueden ser validados. Por favor corríjalos e inténtelo de nuevo." + }, + "3zR9vPNIE1": { + "displayName": "Entrada incorrecta", + "description": "Los datos no pueden ser validados. Por favor corríjalos e inténtelo de nuevo." + }, + "CHRM7IQzo1": { + "displayName": "Cambio fallido", + "description": "El cambio ha fallado. Por favor contacte con su administrador para obtener más ayuda." + }, + "thbsUFokjk": { + "displayName": "Cambio inválido en el identificador", + "description": "Un identificador ha sido cambiado en la entidad. Esto no está permitido." + }, + "40RaYIeeR1": { + "displayName": "No se han efectuado modificaciones.", + "description": "Ningún fichero del repositorio ha sido modificado. Por lo tanto, no se confirmará ninguna modificación." + } + }, + "namespaceStrategies": { + "UsernameNamespaceStrategy": "Nombre de usuario", + "CustomNamespaceStrategy": "Personalizar", + "CurrentYearNamespaceStrategy": "Año actual", + "RepositoryTypeNamespaceStrategy": "Tipo de repositorio" + } +} From ba59713c7f171d66ece39503cd0156a5b66b833c Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:07:48 +0200 Subject: [PATCH 22/48] fix unit test --- scm-webapp/src/test/java/sonia/scm/plugin/PluginTestHelper.java | 2 ++ 1 file changed, 2 insertions(+) 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); } From 38f05fe689e930453997c71bf5bd686427024887 Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:12:49 +0200 Subject: [PATCH 23/48] Prohibit uninstallation of core plugins --- .../java/sonia/scm/plugin/DefaultPluginManager.java | 1 + .../sonia/scm/plugin/DefaultPluginManagerTest.java | 13 +++++++++++++ 2 files changed, 14 insertions(+) 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 6c715c2842..e646bf6372 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -188,6 +188,7 @@ public class DefaultPluginManager implements PluginManager { PluginPermissions.manage().check(); InstalledPlugin installed = getInstalled(name) .orElseThrow(() -> NotFoundException.notFound(entity(InstalledPlugin.class, name))); + doThrow().violation("plugin is a core plugin and cannot be uninstalled").when(installed.isCore()); dependencyTracker.removeInstalled(installed.getDescriptor()); 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 44799a03fb..6e2bb29dba 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -344,6 +344,19 @@ class DefaultPluginManagerTest { assertThat(temp.resolve("uninstall")).exists(); } + + @Test + void shouldThrowExceptionWhenUninstallingCorePlugin(@TempDirectory.TempDir Path temp) { + InstalledPlugin mailPlugin = createInstalled("scm-mail-plugin"); + when(mailPlugin.getDirectory()).thenReturn(temp); + when(mailPlugin.isCore()).thenReturn(true); + + when(loader.getInstalledPlugins()).thenReturn(singletonList(mailPlugin)); + + assertThrows(ScmConstraintViolationException.class, () -> manager.uninstall("scm-mail-plugin", false)); + + assertThat(temp.resolve("uninstall")).doesNotExist(); + } } @Nested From 7ec2b0c31d4cbed7928663f8d4c7fcbabcc99edd Mon Sep 17 00:00:00 2001 From: Rene Pfeuffer Date: Mon, 16 Sep 2019 14:27:56 +0200 Subject: [PATCH 24/48] Delete plugins marked for uninstall --- .../sonia/scm/plugin/InstalledPlugin.java | 2 ++ .../sonia/scm/lifecycle/PluginBootstrap.java | 27 ++++++++++++++++++- .../scm/plugin/DefaultPluginManager.java | 3 +-- 3 files changed, 29 insertions(+), 3 deletions(-) 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 80ad1da7b8..17eb329c9e 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java +++ b/scm-core/src/main/java/sonia/scm/plugin/InstalledPlugin.java @@ -45,6 +45,8 @@ import java.nio.file.Path; public final class InstalledPlugin implements Plugin { + public static final String UNINSTALL_MARKER_FILENAME = "uninstall"; + /** * Constructs a new plugin wrapper. * @param descriptor wrapped plugin diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java index ff8c28f51d..549c50f96d 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PluginBootstrap.java @@ -29,6 +29,7 @@ import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; +import java.nio.file.Path; import java.util.Iterator; import java.util.List; import java.util.Set; @@ -78,12 +79,37 @@ public final class PluginBootstrap { LOG.info("core plugin extraction is disabled"); } + uninstallMarkedPlugins(pluginDirectory.toPath()); return PluginsInternal.collectPlugins(classLoaderLifeCycle, pluginDirectory.toPath()); } catch (IOException ex) { throw new PluginLoadException("could not load plugins", ex); } } + private void uninstallMarkedPlugins(Path pluginDirectory) { + try { + java.nio.file.Files.list(pluginDirectory) + .filter(java.nio.file.Files::isDirectory) + .filter(this::isMarkedForUninstall) + .forEach(this::uninstall); + } catch (IOException e) { + LOG.warn("error occurred while checking for plugins that should be uninstalled", e); + } + } + + private boolean isMarkedForUninstall(Path path) { + return java.nio.file.Files.exists(path.resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); + } + + private void uninstall(Path path) { + try { + LOG.info("deleting plugin directory {}", path); + IOUtil.delete(path.toFile()); + } catch (IOException e) { + LOG.warn("could not delete plugin directory {}", path, e); + } + } + private void renameOldPluginsFolder(File pluginDirectory) { if (new File(pluginDirectory, "classpath.xml").exists()) { File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1"); @@ -96,7 +122,6 @@ public final class PluginBootstrap { } } - private boolean isCorePluginExtractionDisabled() { return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction"); } 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 e646bf6372..b550c1c00a 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -39,7 +39,6 @@ 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; @@ -193,7 +192,7 @@ public class DefaultPluginManager implements PluginManager { dependencyTracker.removeInstalled(installed.getDescriptor()); try { - Files.createFile(installed.getDirectory().resolve("uninstall")); + Files.createFile(installed.getDirectory().resolve(InstalledPlugin.UNINSTALL_MARKER_FILENAME)); } catch (IOException e) { throw new PluginException("could not mark plugin " + name + " in path " + installed.getDirectory() + " for uninstall", e); } From d60918c8209fa7d85e2229d0cb2360b0dac813a0 Mon Sep 17 00:00:00 2001 From: Eduard Heimbuch Date: Mon, 16 Sep 2019 14:31:55 +0200 Subject: [PATCH 25/48] implement update function for plugins on frontend / adjust the plugin pending modal to show pending installations and updates --- .../packages/ui-types/src/Plugin.js | 9 + .../packages/ui-types/src/index.js | 2 +- scm-ui/public/locales/de/admin.json | 16 +- scm-ui/public/locales/en/admin.json | 16 +- ...ndingAction.js => ExecutePendingAction.js} | 18 +- ...PendingModal.js => ExecutePendingModal.js} | 75 +++-- .../{PluginModal.js => InstallPluginModal.js} | 10 +- .../admin/plugins/components/PluginEntry.js | 71 +++-- ...Notification.js => SuccessNotification.js} | 0 .../plugins/components/UpdatePluginModal.js | 288 ++++++++++++++++++ .../plugins/containers/PluginsOverview.js | 64 ++-- scm-ui/src/admin/plugins/modules/plugins.js | 68 ++++- scm-ui/src/modules/indexResource.js | 4 + .../v2/resources/PendingPluginResource.java | 11 +- 14 files changed, 568 insertions(+), 84 deletions(-) rename scm-ui/src/admin/plugins/components/{InstallPendingAction.js => ExecutePendingAction.js} (67%) rename scm-ui/src/admin/plugins/components/{InstallPendingModal.js => ExecutePendingModal.js} (55%) rename scm-ui/src/admin/plugins/components/{PluginModal.js => InstallPluginModal.js} (96%) rename scm-ui/src/admin/plugins/components/{InstallSuccessNotification.js => SuccessNotification.js} (100%) create mode 100644 scm-ui/src/admin/plugins/components/UpdatePluginModal.js diff --git a/scm-ui-components/packages/ui-types/src/Plugin.js b/scm-ui-components/packages/ui-types/src/Plugin.js index 0114716757..f7175fc209 100644 --- a/scm-ui-components/packages/ui-types/src/Plugin.js +++ b/scm-ui-components/packages/ui-types/src/Plugin.js @@ -4,6 +4,7 @@ import type {Collection, Links} from "./hal"; export type Plugin = { name: string, version: string, + newVersion: string, displayName: string, description?: string, author: string, @@ -24,3 +25,11 @@ export type PluginGroup = { name: string, plugins: Plugin[] }; + +export type PendingPlugins = { + _links: Links, + _embedded: { + new: [], + update: [], + } +} diff --git a/scm-ui-components/packages/ui-types/src/index.js b/scm-ui-components/packages/ui-types/src/index.js index ba2b9f5481..9207868a73 100644 --- a/scm-ui-components/packages/ui-types/src/index.js +++ b/scm-ui-components/packages/ui-types/src/index.js @@ -25,7 +25,7 @@ export type { SubRepository, File } from "./Sources"; export type { SelectValue, AutocompleteObject } from "./Autocomplete"; -export type { Plugin, PluginCollection, PluginGroup } from "./Plugin"; +export type { Plugin, PluginCollection, PluginGroup, PendingPlugins } from "./Plugin"; export type { RepositoryRole } from "./RepositoryRole"; diff --git a/scm-ui/public/locales/de/admin.json b/scm-ui/public/locales/de/admin.json index 8d9c876537..d2cd2c6640 100644 --- a/scm-ui/public/locales/de/admin.json +++ b/scm-ui/public/locales/de/admin.json @@ -29,22 +29,32 @@ "installedNavLink": "Installiert", "availableNavLink": "Verfügbar" }, - "installPending": "Austehende Plugins installieren", + "executePending": "Austehende Plugin-Änderungen ausführen", "noPlugins": "Keine Plugins gefunden.", "modal": { - "title": "{{name}} Plugin installieren", + "title": { + "install": "{{name}} Plugin installieren", + "update": "{{name}} Plugin aktualisieren" + }, "restart": "Neustarten um Plugin zu aktivieren", "install": "Installieren", + "update": "Aktualisieren", + "installQueue": "Werden installiert:", + "updateQueue": "Werden aktualisiert:", "installAndRestart": "Installieren und Neustarten", + "updateAndRestart": "Aktualisieren und Neustarten", + "executeAndRestart": "Ausführen und Neustarten", "abort": "Abbrechen", "author": "Autor", "version": "Version", + "currentVersion": "Installierte Version", + "newVersion": "Neue Version", "dependencyNotification": "Mit diesem Plugin werden folgende Abhängigkeiten mit installiert, wenn sie noch nicht vorhanden sind!", "dependencies": "Abhängigkeiten", "successNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", "reload": "jetzt neu laden", "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", - "installPending": "Die folgenden Plugins werden installiert. Anschließend wird der SCM-Manager Kontext neu gestartet." + "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet." } }, "repositoryRole": { diff --git a/scm-ui/public/locales/en/admin.json b/scm-ui/public/locales/en/admin.json index 99febb68fc..63f6089b09 100644 --- a/scm-ui/public/locales/en/admin.json +++ b/scm-ui/public/locales/en/admin.json @@ -29,22 +29,32 @@ "installedNavLink": "Installed", "availableNavLink": "Available" }, - "installPending": "Install pending plugins", + "executePending": "Execute pending plugin changes", "noPlugins": "No plugins found.", "modal": { - "title": "Install {{name}} Plugin", + "title": { + "install": "Install {{name}} Plugin", + "update": "Update {{name}} Plugin" + }, "restart": "Restart to activate", "install": "Install", + "update": "Update", + "installQueue": "Will be installed:", + "updateQueue": "Will be updated:", "installAndRestart": "Install and Restart", + "updateAndRestart": "Update and Restart", + "executeAndRestart": "Execute and Restart", "abort": "Abort", "author": "Author", "version": "Version", + "currentVersion": "Installed version", + "newVersion": "New version", "dependencyNotification": "With this plugin, the following dependencies will be installed if they are not available yet!", "dependencies": "Dependencies", "successNotification": "Successful installed plugin. You have to reload the page, to see ui changes:", "reload": "reload now", "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", - "installPending": "The following plugins will be installed and after installation the scm-manager context will be restarted." + "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted." } }, "repositoryRole": { diff --git a/scm-ui/src/admin/plugins/components/InstallPendingAction.js b/scm-ui/src/admin/plugins/components/ExecutePendingAction.js similarity index 67% rename from scm-ui/src/admin/plugins/components/InstallPendingAction.js rename to scm-ui/src/admin/plugins/components/ExecutePendingAction.js index 49a444de11..6c8d407205 100644 --- a/scm-ui/src/admin/plugins/components/InstallPendingAction.js +++ b/scm-ui/src/admin/plugins/components/ExecutePendingAction.js @@ -1,12 +1,12 @@ // @flow import React from "react"; import { Button } from "@scm-manager/ui-components"; -import type { PluginCollection } from "@scm-manager/ui-types"; +import type { PendingPlugins } from "@scm-manager/ui-types"; import { translate } from "react-i18next"; -import InstallPendingModal from "./InstallPendingModal"; +import ExecutePendingModal from "./ExecutePendingModal"; type Props = { - collection: PluginCollection, + pendingPlugins: PendingPlugins, // context props t: string => string @@ -16,7 +16,7 @@ type State = { showModal: boolean }; -class InstallPendingAction extends React.Component { +class ExecutePendingAction extends React.Component { constructor(props: Props) { super(props); this.state = { @@ -38,11 +38,11 @@ class InstallPendingAction extends React.Component { renderModal = () => { const { showModal } = this.state; - const { collection } = this.props; + const { pendingPlugins } = this.props; if (showModal) { return ( - ); @@ -57,7 +57,7 @@ class InstallPendingAction extends React.Component { {this.renderModal()}