diff --git a/gradle/changelog/config_change_plugin_center_invalidation.yaml b/gradle/changelog/config_change_plugin_center_invalidation.yaml new file mode 100644 index 0000000000..2f3f7b03b6 --- /dev/null +++ b/gradle/changelog/config_change_plugin_center_invalidation.yaml @@ -0,0 +1,4 @@ +- type: fixed + description: Invalidate plugin center cache on global configuration change ([#2147](https://github.com/scm-manager/scm-manager/pull/2147)) +- type: changed + description: Provide feedback on plugin center status ([#2147](https://github.com/scm-manager/scm-manager/pull/2147)) diff --git a/scm-core/src/main/java/sonia/scm/plugin/PluginCenterStatus.java b/scm-core/src/main/java/sonia/scm/plugin/PluginCenterStatus.java new file mode 100644 index 0000000000..5d14644624 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginCenterStatus.java @@ -0,0 +1,34 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.plugin; + +/** + * @since 2.40.0 + */ +public enum PluginCenterStatus { + OK, + ERROR, + DEACTIVATED +} 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 4720375e0f..9e27634a2b 100644 --- a/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java +++ b/scm-core/src/main/java/sonia/scm/plugin/PluginManager.java @@ -24,6 +24,10 @@ package sonia.scm.plugin; +import com.google.common.annotations.VisibleForTesting; +import lombok.AllArgsConstructor; +import lombok.Value; + import java.util.List; import java.util.Optional; import java.util.Set; @@ -57,6 +61,13 @@ public interface PluginManager { */ List getInstalled(); + /** + * @since 2.40.0 + */ + default PluginResult getPlugins() { + return new PluginResult(getInstalled(), getAvailable()); + } + /** * Returns all available plugins. The list contains the plugins which are loaded from the plugin center, but without * the installed plugins. @@ -127,4 +138,22 @@ public interface PluginManager { * Update all installed plugins. */ void updateAll(); + + /** + * Returned by {@link #getPlugins()}. + * @since 2.40.0 + */ + @Value + @AllArgsConstructor + class PluginResult { + List installedPlugins; + List availablePlugins; + PluginCenterStatus pluginCenterStatus; + + @VisibleForTesting + public PluginResult(List installedPlugins, List availablePlugins) { + this(installedPlugins, availablePlugins, PluginCenterStatus.OK); + } + + } } diff --git a/scm-ui/ui-api/src/plugins.test.ts b/scm-ui/ui-api/src/plugins.test.ts index eb8acad819..adf7e27f81 100644 --- a/scm-ui/ui-api/src/plugins.test.ts +++ b/scm-ui/ui-api/src/plugins.test.ts @@ -114,6 +114,7 @@ describe("Test plugin hooks", () => { _embedded: { plugins, }, + pluginCenterStatus: "OK", }); const createPendingPlugins = ( diff --git a/scm-ui/ui-types/src/Plugin.ts b/scm-ui/ui-types/src/Plugin.ts index 75fcd57eaf..6e33bc6f2e 100644 --- a/scm-ui/ui-types/src/Plugin.ts +++ b/scm-ui/ui-types/src/Plugin.ts @@ -51,9 +51,11 @@ export type Plugin = HalRepresentation & { optionalDependencies: string[]; }; +export type PluginCenterStatus = "OK" | "ERROR" | "DEACTIVATED"; + export type PluginCollection = HalRepresentationWithEmbedded<{ plugins: Plugin[]; -}>; +}> & { pluginCenterStatus: PluginCenterStatus }; export const isPluginCollection = (input: HalRepresentation): input is PluginCollection => input._embedded ? "plugins" in input._embedded : false; diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index 7f7481bbb9..fe55c03faa 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -44,6 +44,10 @@ "updateAll": "Alle Plugins aktualisieren", "cancelPending": "Änderungen abbrechen", "noPlugins": "Keine Plugins gefunden.", + "pluginCenterStatus": { + "ERROR": "Das Plugin Center ist nicht verfügbar. Plugins können weder installiert noch aktualisiert werden.", + "DEACTIVATED": "Das Plugin Center wurde in der Konfiguration deaktiviert. Plugins können weder installiert noch aktualisiert werden." + }, "modal": { "title": { "install": "{{name}} Plugin installieren", diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index fef458d237..ceae39de19 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -44,6 +44,10 @@ "updateAll": "Update All Plugins", "cancelPending": "Cancel Changes", "noPlugins": "No plugins found.", + "pluginCenterStatus": { + "ERROR": "The Plugin Center is not available. Plugins can neither be installed nor updated.", + "DEACTIVATED": "The Plugin Center is disabled in the configuration. Plugins can neither be installed nor updated." + }, "modal": { "title": { "install": "Install {{name}} Plugin", diff --git a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx index b737c11835..1c983be93c 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/containers/PluginsOverview.tsx @@ -32,7 +32,7 @@ import { Loading, Notification, Subtitle, - Title + Title, } from "@scm-manager/ui-components"; import PluginsList from "../components/PluginList"; import PluginTopActions from "../components/PluginTopActions"; @@ -45,7 +45,7 @@ import { useAvailablePlugins, useInstalledPlugins, usePendingPlugins, - usePluginCenterAuthInfo + usePluginCenterAuthInfo, } from "@scm-manager/ui-api"; import PluginModal from "../components/PluginModal"; import MyCloudoguBanner from "../components/MyCloudoguBanner"; @@ -55,7 +55,7 @@ export enum PluginAction { INSTALL = "install", UPDATE = "update", UNINSTALL = "uninstall", - CLOUDOGU = "cloudoguInstall" + CLOUDOGU = "cloudoguInstall", } export type PluginModalContent = { @@ -72,12 +72,12 @@ const PluginsOverview: FC = ({ installed }) => { const { data: availablePlugins, isLoading: isLoadingAvailablePlugins, - error: availablePluginsError + error: availablePluginsError, } = useAvailablePlugins({ enabled: !installed }); const { data: installedPlugins, isLoading: isLoadingInstalledPlugins, - error: installedPluginsError + error: installedPluginsError, } = useInstalledPlugins({ enabled: installed }); const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins(); const pluginCenterAuthInfo = usePluginCenterAuthInfo(); @@ -177,21 +177,31 @@ const PluginsOverview: FC = ({ installed }) => { const computeUpdateAllSize = () => { const outdatedPlugins = collection?._embedded?.plugins.filter((p: Plugin) => p._links.update).length; return t("plugins.outdatedPlugins", { - count: outdatedPlugins + count: outdatedPlugins, }); }; const renderPluginsList = () => { - if (collection?._embedded && collection._embedded.plugins.length > 0) { - return ( - + let pluginCenterStatusNotification: React.ReactNode; + if (collection && collection.pluginCenterStatus !== "OK") { + const type = collection.pluginCenterStatus === "DEACTIVATED" ? "info" : "danger"; + pluginCenterStatusNotification = ( + {t(`plugins.pluginCenterStatus.${collection.pluginCenterStatus}`)} ); } - return {t("plugins.noPlugins")}; + if (collection?._embedded && collection._embedded.plugins.length > 0) { + return ( + <> + {pluginCenterStatusNotification} + + + ); + } + return pluginCenterStatusNotification ?? {t("plugins.noPlugins")}; }; const renderModals = () => { 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 ee9ca92229..9444675580 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 @@ -95,10 +95,10 @@ public class AvailablePluginResource { @Produces(VndMediaType.PLUGIN_COLLECTION) public Response getAvailablePlugins() { PluginPermissions.read().check(); - List installed = pluginManager.getInstalled(); - List available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList()); + PluginManager.PluginResult plugins = pluginManager.getPlugins(); + List available = plugins.getAvailablePlugins().stream().filter(a -> notInstalled(a, plugins.getInstalledPlugins())).collect(Collectors.toList()); - return Response.ok(collectionMapper.mapAvailable(available)).build(); + return Response.ok(collectionMapper.mapAvailable(available, plugins.getPluginCenterStatus())).build(); } private boolean notInstalled(AvailablePlugin a, List installed) { 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 c1c598d962..90ef2fbc97 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 @@ -95,9 +95,8 @@ public class InstalledPluginResource { ) public Response getInstalledPlugins() { PluginPermissions.read().check(); - List plugins = pluginManager.getInstalled(); - List available = pluginManager.getAvailable(); - return Response.ok(collectionMapper.mapInstalled(plugins, available)).build(); + PluginManager.PluginResult plugins = pluginManager.getPlugins(); + return Response.ok(collectionMapper.mapInstalled(plugins)).build(); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginCollectionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginCollectionDto.java new file mode 100644 index 0000000000..7cb1a53810 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginCollectionDto.java @@ -0,0 +1,44 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import sonia.scm.plugin.PluginCenterStatus; + +@Getter +@NoArgsConstructor +public class PluginCollectionDto extends HalRepresentation { + + private PluginCenterStatus pluginCenterStatus; + + public PluginCollectionDto(Links links, Embedded embedded, PluginCenterStatus pluginCenterStatus) { + super(links, embedded); + this.pluginCenterStatus = pluginCenterStatus; + } +} 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 ff66f2bc85..fe03caa2fe 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,10 +26,9 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; 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.PluginCenterStatus; import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginPermissions; @@ -53,17 +52,18 @@ public class PluginDtoCollectionMapper { this.manager = manager; } - public HalRepresentation mapInstalled(List plugins, List availablePlugins) { + public PluginCollectionDto mapInstalled(PluginManager.PluginResult plugins) { List dtos = plugins + .getInstalledPlugins() .stream() - .map(i -> mapper.mapInstalled(i, availablePlugins)) + .map(i -> mapper.mapInstalled(i, plugins.getAvailablePlugins())) .collect(toList()); - return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos)); + return new PluginCollectionDto(createInstalledPluginsLinks(), embedDtos(dtos), plugins.getPluginCenterStatus()); } - public HalRepresentation mapAvailable(List plugins) { + public PluginCollectionDto mapAvailable(List plugins, PluginCenterStatus pluginCenterStatus) { List dtos = plugins.stream().map(mapper::mapAvailable).collect(toList()); - return new HalRepresentation(createAvailablePluginsLinks(plugins), embedDtos(dtos)); + return new PluginCollectionDto(createAvailablePluginsLinks(plugins), embedDtos(dtos), pluginCenterStatus); } private Links createInstalledPluginsLinks() { 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 fd47cf5fdf..d9a4349eaf 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -107,6 +107,17 @@ public class DefaultPluginManager implements PluginManager { updateMayUninstallFlag(); } + @Override + public PluginResult getPlugins() { + PluginPermissions.read().check(); + PluginCenterResult pluginCenterResult = center.getPluginResult(); + return new PluginResult( + getInstalled(), + filterNotInstalledOrMoreUpToDate(pluginCenterResult.getPlugins()), + pluginCenterResult.getStatus() + ); + } + @Override public Optional getAvailable(String name) { PluginPermissions.read().check(); @@ -144,7 +155,11 @@ public class DefaultPluginManager implements PluginManager { @Override public List getAvailable() { PluginPermissions.read().check(); - return center.getAvailablePlugins() + return filterNotInstalledOrMoreUpToDate(center.getAvailablePlugins()); + } + + private List filterNotInstalledOrMoreUpToDate(Set availablePlugins) { + return availablePlugins .stream() .filter(this::isNotInstalledOrMoreUpToDate) .map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p)) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java index fa5eb3d7d1..85cbf163b7 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenter.java @@ -32,6 +32,7 @@ import sonia.scm.SCMContextProvider; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.config.ScmConfiguration; +import sonia.scm.config.ScmConfigurationChangedEvent; import sonia.scm.util.HttpUtil; import sonia.scm.util.SystemUtil; @@ -65,6 +66,12 @@ public class PluginCenter { pluginCenterResultCache.clear(); } + @Subscribe + public void handle(ScmConfigurationChangedEvent event) { + LOG.debug("clear plugin center cache, because of {}", event); + pluginCenterResultCache.clear(); + } + synchronized Set getAvailablePlugins() { String url = buildPluginUrl(configuration.getPluginUrl()); return getPluginCenterResult(url).getPlugins(); @@ -75,6 +82,11 @@ public class PluginCenter { return getPluginCenterResult(url).getPluginSets(); } + synchronized PluginCenterResult getPluginResult() { + String url = buildPluginUrl(configuration.getPluginUrl()); + return getPluginCenterResult(url); + } + private PluginCenterResult getPluginCenterResult(String url) { PluginCenterResult pluginCenterResult = pluginCenterResultCache.get(url); if (pluginCenterResult == null) { diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java index e519657568..60cf4deb81 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterLoader.java @@ -25,6 +25,7 @@ package sonia.scm.plugin; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.event.ScmEventBus; @@ -32,7 +33,6 @@ import sonia.scm.net.ahc.AdvancedHttpClient; import sonia.scm.net.ahc.AdvancedHttpRequest; import javax.inject.Inject; -import java.util.Collections; import static sonia.scm.plugin.Tracing.SPAN_KIND; @@ -65,6 +65,10 @@ class PluginCenterLoader { PluginCenterResult load(String url) { try { + if (Strings.isNullOrEmpty(url)) { + LOG.info("plugin center is deactivated, returning empty list"); + return new PluginCenterResult(PluginCenterStatus.DEACTIVATED); + } LOG.info("fetch plugins from {}", url); AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND); if (authenticator.isAuthenticated()) { @@ -75,7 +79,7 @@ class PluginCenterLoader { } catch (Exception ex) { LOG.error("failed to load plugins from plugin center, returning empty list", ex); eventBus.post(new PluginCenterErrorEvent()); - return new PluginCenterResult(Collections.emptySet(), Collections.emptySet()); + return new PluginCenterResult(PluginCenterStatus.ERROR); } } } diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterResult.java b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterResult.java index e23e44e814..fe96be1ec7 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterResult.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PluginCenterResult.java @@ -27,6 +27,7 @@ package sonia.scm.plugin; import lombok.AllArgsConstructor; import lombok.Getter; +import java.util.Collections; import java.util.Set; @AllArgsConstructor @@ -34,4 +35,18 @@ import java.util.Set; class PluginCenterResult { private Set plugins; private Set pluginSets; + private PluginCenterStatus status; + + public PluginCenterResult() { + this(Collections.emptySet(), Collections.emptySet(), PluginCenterStatus.OK); + } + + public PluginCenterResult(PluginCenterStatus status) { + this(Collections.emptySet(), Collections.emptySet(), status); + } + + public PluginCenterResult(Set plugins, Set pluginSets) { + this(plugins, pluginSets, PluginCenterStatus.OK); + } + } 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 8718dd10c4..5aee7eb977 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 @@ -24,7 +24,6 @@ package sonia.scm.api.v2.resources; -import de.otto.edison.hal.HalRepresentation; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; @@ -42,6 +41,7 @@ import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginCenterStatus; import sonia.scm.plugin.PluginCondition; import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; @@ -116,9 +116,9 @@ class AvailablePluginResourceTest { void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException { 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()); + when(pluginManager.getPlugins()).thenReturn(new PluginManager.PluginResult(Collections.emptyList(), Collections.singletonList(plugin))); + + when(collectionMapper.mapAvailable(Collections.singletonList(plugin), PluginCenterStatus.OK)).thenReturn(new MockedResultDto()); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); request.accept(VndMediaType.PLUGIN_COLLECTION); @@ -135,9 +135,8 @@ class AvailablePluginResourceTest { 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()); + when(pluginManager.getPlugins()).thenReturn(new PluginManager.PluginResult(Collections.singletonList(installedPlugin), Collections.singletonList(availablePlugin))); + lenient().when(collectionMapper.mapAvailable(Collections.singletonList(availablePlugin), PluginCenterStatus.OK)).thenReturn(new MockedResultDto()); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); request.accept(VndMediaType.PLUGIN_COLLECTION); @@ -261,7 +260,7 @@ class AvailablePluginResourceTest { } } - public class MockedResultDto extends HalRepresentation { + public class MockedResultDto extends PluginCollectionDto { public String getMarker() { return "x"; } 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 f1fcb1b559..592e10e066 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 @@ -24,7 +24,6 @@ package sonia.scm.api.v2.resources; -import de.otto.edison.hal.HalRepresentation; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ThreadContext; @@ -110,8 +109,9 @@ class InstalledPluginResourceTest { @Test void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { InstalledPlugin installedPlugin = createInstalled(""); - when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); - when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin), Collections.emptyList())).thenReturn(new MockedResultDto()); + PluginManager.PluginResult pluginResult = new PluginManager.PluginResult(Collections.singletonList(installedPlugin), emptyList()); + when(pluginManager.getPlugins()).thenReturn(pluginResult); + when(collectionMapper.mapInstalled(pluginResult)).thenReturn(new MockedResultDto()); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); request.accept(VndMediaType.PLUGIN_COLLECTION); @@ -184,7 +184,8 @@ class InstalledPluginResourceTest { } } - public class MockedResultDto extends HalRepresentation { + public class MockedResultDto extends PluginCollectionDto { + public String getMarker() { return "x"; } 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 index f91c7b08b9..2a5fbeb697 100644 --- 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 @@ -41,14 +41,15 @@ import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPluginDescriptor; +import sonia.scm.plugin.PluginCenterStatus; import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginManager; import java.net.URI; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; +import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.lenient; @@ -88,14 +89,23 @@ class PluginDtoCollectionMapperTest { ThreadContext.unbindSubject(); } + @Test + void shouldMapErrorStatus() { + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); + + assertThat(mapper.mapInstalled(emptyPluginResult(PluginCenterStatus.ERROR)).getPluginCenterStatus()).isEqualTo(PluginCenterStatus.ERROR); + assertThat(mapper.mapInstalled(emptyPluginResult(PluginCenterStatus.OK)).getPluginCenterStatus()).isEqualTo(PluginCenterStatus.OK); + assertThat(mapper.mapAvailable(emptyList(), PluginCenterStatus.ERROR).getPluginCenterStatus()).isEqualTo(PluginCenterStatus.ERROR); + assertThat(mapper.mapAvailable(emptyList(), PluginCenterStatus.OK).getPluginCenterStatus()).isEqualTo(PluginCenterStatus.OK); + } @Test void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() { PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(createAvailablePlugin("scm-other-plugin", "2"))); + singletonList(createAvailablePlugin("scm-other-plugin", "2")))); List plugins = result.getEmbedded().getItemsBy("plugins"); assertThat(plugins).hasSize(1); @@ -106,11 +116,11 @@ class PluginDtoCollectionMapperTest { @Test void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() { - PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper,manager); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + singletonList(createAvailablePlugin("scm-some-plugin", "2")))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.getVersion()).isEqualTo("1"); @@ -122,9 +132,9 @@ class PluginDtoCollectionMapperTest { when(subject.isPermitted("plugin:write")).thenReturn(false); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + singletonList(createAvailablePlugin("scm-some-plugin", "2")))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); @@ -137,9 +147,9 @@ class PluginDtoCollectionMapperTest { AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); when(availablePlugin.isPending()).thenReturn(true); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(availablePlugin)); + singletonList(availablePlugin))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); @@ -150,9 +160,9 @@ class PluginDtoCollectionMapperTest { when(subject.isPermitted("plugin:write")).thenReturn(true); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + singletonList(createAvailablePlugin("scm-some-plugin", "2")))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); @@ -164,9 +174,9 @@ class PluginDtoCollectionMapperTest { when(subject.isPermitted("plugin:write")).thenReturn(true); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + singletonList(createAvailablePlugin("scm-some-plugin", "2")))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); @@ -180,9 +190,9 @@ class PluginDtoCollectionMapperTest { AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); when(availablePlugin.isPending()).thenReturn(true); - HalRepresentation result = mapper.mapInstalled( + HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult( singletonList(createInstalledPlugin("scm-some-plugin", "1")), - singletonList(availablePlugin)); + singletonList(availablePlugin))); PluginDto plugin = getPluginDtoFromResult(result); assertThat(plugin.isPending()).isTrue(); @@ -223,4 +233,12 @@ class PluginDtoCollectionMapperTest { lenient().when(plugin.getDescriptor()).thenReturn(descriptor); return plugin; } + + private static PluginManager.PluginResult emptyPluginResult(PluginCenterStatus status) { + return new PluginManager.PluginResult( + emptyList(), + emptyList(), + status + ); + } } 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 698e2cfd40..e4459d11f6 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -54,6 +54,7 @@ import java.util.Optional; import java.util.Set; import static java.util.Arrays.asList; +import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; @@ -133,6 +134,27 @@ class DefaultPluginManagerTest { ThreadContext.unbindSubject(); } + @Test + void shouldReturnSuccessfulPluginResult() { + AvailablePlugin editor = createAvailable("scm-editor-plugin"); + AvailablePlugin jenkins = createAvailable("scm-jenkins-plugin"); + InstalledPlugin review = createInstalled("scm-review-plugin"); + InstalledPlugin git = createInstalled("scm-git-plugin"); + + when(center.getPluginResult()).thenReturn(new PluginCenterResult( + ImmutableSet.of(editor, jenkins), + emptySet()) + ); + + when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(review, git)); + + PluginManager.PluginResult plugins = manager.getPlugins(); + + assertThat(plugins.getAvailablePlugins()).containsOnly(editor, jenkins); + assertThat(plugins.getInstalledPlugins()).containsOnly(review, git); + assertThat(plugins.getPluginCenterStatus()).isEqualTo(PluginCenterStatus.OK); + } + @Test void shouldReturnInstalledPlugins() { InstalledPlugin review = createInstalled("scm-review-plugin"); @@ -746,6 +768,7 @@ class DefaultPluginManagerTest { assertThrows(AuthorizationException.class, () -> manager.getAvailable()); assertThrows(AuthorizationException.class, () -> manager.getAvailable("test")); assertThrows(AuthorizationException.class, () -> manager.getPluginSets()); + assertThrows(AuthorizationException.class, () -> manager.getPlugins()); } } diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java index 6d60d7e9af..a21558c8fe 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterLoaderTest.java @@ -79,6 +79,7 @@ class PluginCenterLoaderTest { PluginCenterResult fetched = loader.load(PLUGIN_URL); assertThat(fetched.getPlugins()).isSameAs(plugins); assertThat(fetched.getPluginSets()).isSameAs(pluginSets); + assertThat(fetched.getStatus()).isEqualTo(PluginCenterStatus.OK); } private AdvancedHttpResponse request() throws IOException { @@ -88,6 +89,14 @@ class PluginCenterLoaderTest { return response; } + @Test + void shouldReturnEmptySetIfPluginCenterIsDeactivated() { + PluginCenterResult fetch = loader.load(""); + assertThat(fetch.getPlugins()).isEmpty(); + assertThat(fetch.getPluginSets()).isEmpty(); + assertThat(fetch.getStatus()).isSameAs(PluginCenterStatus.DEACTIVATED); + } + @Test void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException { when(client.get(PLUGIN_URL)).thenReturn(request); @@ -96,6 +105,7 @@ class PluginCenterLoaderTest { PluginCenterResult fetch = loader.load(PLUGIN_URL); assertThat(fetch.getPlugins()).isEmpty(); assertThat(fetch.getPluginSets()).isEmpty(); + assertThat(fetch.getStatus()).isSameAs(PluginCenterStatus.ERROR); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java index ad03202b5a..94f807773d 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/PluginCenterTest.java @@ -33,8 +33,8 @@ import sonia.scm.SCMContextProvider; import sonia.scm.cache.CacheManager; import sonia.scm.cache.MapCacheManager; import sonia.scm.config.ScmConfiguration; +import sonia.scm.config.ScmConfigurationChangedEvent; -import java.util.Collections; import java.util.HashSet; import java.util.Set; @@ -92,7 +92,7 @@ class PluginCenterTest { Set pluginSets = new HashSet<>(); PluginCenterResult first = new PluginCenterResult(plugins, pluginSets); - when(loader.load(anyString())).thenReturn(first, new PluginCenterResult(Collections.emptySet(), Collections.emptySet())); + when(loader.load(anyString())).thenReturn(first, new PluginCenterResult()); assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins); assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins); @@ -101,12 +101,12 @@ class PluginCenterTest { @Test @SuppressWarnings("unchecked") - void shouldClearCache() { + void shouldClearCacheOnPluginCenterLogin() { Set plugins = new HashSet<>(); Set pluginSets = new HashSet<>(); PluginCenterResult first = new PluginCenterResult(plugins, pluginSets); - when(loader.load(anyString())).thenReturn(first, new PluginCenterResult(Collections.emptySet(), Collections.emptySet())); + when(loader.load(anyString())).thenReturn(first, new PluginCenterResult()); assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins); assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets); @@ -115,6 +115,22 @@ class PluginCenterTest { assertThat(pluginCenter.getAvailablePluginSets()).isNotSameAs(pluginSets); } + @Test + @SuppressWarnings("unchecked") + void shouldClearCacheOnConfigChange() { + Set plugins = new HashSet<>(); + Set pluginSets = new HashSet<>(); + + PluginCenterResult first = new PluginCenterResult(plugins, pluginSets); + when(loader.load(anyString())).thenReturn(first, new PluginCenterResult()); + + assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins); + assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets); + pluginCenter.handle(new ScmConfigurationChangedEvent(null)); + assertThat(pluginCenter.getAvailablePlugins()).isNotSameAs(plugins); + assertThat(pluginCenter.getAvailablePluginSets()).isNotSameAs(pluginSets); + } + @Test void shouldLoadOnRefresh() { Set plugins = new HashSet<>();