From 7b933c6821539a41ee9035f8a7911eec0a7bfd04 Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Fri, 4 Nov 2022 11:49:08 +0100 Subject: [PATCH] Improve plugin center error feedback and cache invalidation (#2147) The plugin center cache was not invalidated when the proxy configuration was changed in the global settings. This caused stale and inconsistent state to be displayed to the user while there was no feedback that something was wrong. --- ...fig_change_plugin_center_invalidation.yaml | 4 ++ .../sonia/scm/plugin/PluginCenterStatus.java | 34 +++++++++++++ .../java/sonia/scm/plugin/PluginManager.java | 29 +++++++++++ scm-ui/ui-api/src/plugins.test.ts | 1 + scm-ui/ui-types/src/Plugin.ts | 4 +- scm-ui/ui-webapp/public/locales/de/admin.json | 4 ++ scm-ui/ui-webapp/public/locales/en/admin.json | 4 ++ .../plugins/containers/PluginsOverview.tsx | 38 ++++++++------ .../v2/resources/AvailablePluginResource.java | 6 +-- .../v2/resources/InstalledPluginResource.java | 5 +- .../api/v2/resources/PluginCollectionDto.java | 44 ++++++++++++++++ .../resources/PluginDtoCollectionMapper.java | 14 +++--- .../scm/plugin/DefaultPluginManager.java | 17 ++++++- .../java/sonia/scm/plugin/PluginCenter.java | 12 +++++ .../sonia/scm/plugin/PluginCenterLoader.java | 8 ++- .../sonia/scm/plugin/PluginCenterResult.java | 15 ++++++ .../AvailablePluginResourceTest.java | 15 +++--- .../InstalledPluginResourceTest.java | 9 ++-- .../PluginDtoCollectionMapperTest.java | 50 +++++++++++++------ .../scm/plugin/DefaultPluginManagerTest.java | 23 +++++++++ .../scm/plugin/PluginCenterLoaderTest.java | 10 ++++ .../sonia/scm/plugin/PluginCenterTest.java | 24 +++++++-- 22 files changed, 307 insertions(+), 63 deletions(-) create mode 100644 gradle/changelog/config_change_plugin_center_invalidation.yaml create mode 100644 scm-core/src/main/java/sonia/scm/plugin/PluginCenterStatus.java create mode 100644 scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginCollectionDto.java 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<>();