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.
This commit is contained in:
Konstantin Schaper
2022-11-04 11:49:08 +01:00
committed by GitHub
parent ec83de3600
commit 7b933c6821
22 changed files with 307 additions and 63 deletions

View File

@@ -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))

View File

@@ -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
}

View File

@@ -24,6 +24,10 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting;
import lombok.AllArgsConstructor;
import lombok.Value;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
@@ -57,6 +61,13 @@ public interface PluginManager {
*/ */
List<InstalledPlugin> getInstalled(); List<InstalledPlugin> 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 * Returns all available plugins. The list contains the plugins which are loaded from the plugin center, but without
* the installed plugins. * the installed plugins.
@@ -127,4 +138,22 @@ public interface PluginManager {
* Update all installed plugins. * Update all installed plugins.
*/ */
void updateAll(); void updateAll();
/**
* Returned by {@link #getPlugins()}.
* @since 2.40.0
*/
@Value
@AllArgsConstructor
class PluginResult {
List<InstalledPlugin> installedPlugins;
List<AvailablePlugin> availablePlugins;
PluginCenterStatus pluginCenterStatus;
@VisibleForTesting
public PluginResult(List<InstalledPlugin> installedPlugins, List<AvailablePlugin> availablePlugins) {
this(installedPlugins, availablePlugins, PluginCenterStatus.OK);
}
}
} }

View File

@@ -114,6 +114,7 @@ describe("Test plugin hooks", () => {
_embedded: { _embedded: {
plugins, plugins,
}, },
pluginCenterStatus: "OK",
}); });
const createPendingPlugins = ( const createPendingPlugins = (

View File

@@ -51,9 +51,11 @@ export type Plugin = HalRepresentation & {
optionalDependencies: string[]; optionalDependencies: string[];
}; };
export type PluginCenterStatus = "OK" | "ERROR" | "DEACTIVATED";
export type PluginCollection = HalRepresentationWithEmbedded<{ export type PluginCollection = HalRepresentationWithEmbedded<{
plugins: Plugin[]; plugins: Plugin[];
}>; }> & { pluginCenterStatus: PluginCenterStatus };
export const isPluginCollection = (input: HalRepresentation): input is PluginCollection => export const isPluginCollection = (input: HalRepresentation): input is PluginCollection =>
input._embedded ? "plugins" in input._embedded : false; input._embedded ? "plugins" in input._embedded : false;

View File

@@ -44,6 +44,10 @@
"updateAll": "Alle Plugins aktualisieren", "updateAll": "Alle Plugins aktualisieren",
"cancelPending": "Änderungen abbrechen", "cancelPending": "Änderungen abbrechen",
"noPlugins": "Keine Plugins gefunden.", "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": { "modal": {
"title": { "title": {
"install": "{{name}} Plugin installieren", "install": "{{name}} Plugin installieren",

View File

@@ -44,6 +44,10 @@
"updateAll": "Update All Plugins", "updateAll": "Update All Plugins",
"cancelPending": "Cancel Changes", "cancelPending": "Cancel Changes",
"noPlugins": "No plugins found.", "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": { "modal": {
"title": { "title": {
"install": "Install {{name}} Plugin", "install": "Install {{name}} Plugin",

View File

@@ -32,7 +32,7 @@ import {
Loading, Loading,
Notification, Notification,
Subtitle, Subtitle,
Title Title,
} from "@scm-manager/ui-components"; } from "@scm-manager/ui-components";
import PluginsList from "../components/PluginList"; import PluginsList from "../components/PluginList";
import PluginTopActions from "../components/PluginTopActions"; import PluginTopActions from "../components/PluginTopActions";
@@ -45,7 +45,7 @@ import {
useAvailablePlugins, useAvailablePlugins,
useInstalledPlugins, useInstalledPlugins,
usePendingPlugins, usePendingPlugins,
usePluginCenterAuthInfo usePluginCenterAuthInfo,
} from "@scm-manager/ui-api"; } from "@scm-manager/ui-api";
import PluginModal from "../components/PluginModal"; import PluginModal from "../components/PluginModal";
import MyCloudoguBanner from "../components/MyCloudoguBanner"; import MyCloudoguBanner from "../components/MyCloudoguBanner";
@@ -55,7 +55,7 @@ export enum PluginAction {
INSTALL = "install", INSTALL = "install",
UPDATE = "update", UPDATE = "update",
UNINSTALL = "uninstall", UNINSTALL = "uninstall",
CLOUDOGU = "cloudoguInstall" CLOUDOGU = "cloudoguInstall",
} }
export type PluginModalContent = { export type PluginModalContent = {
@@ -72,12 +72,12 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const { const {
data: availablePlugins, data: availablePlugins,
isLoading: isLoadingAvailablePlugins, isLoading: isLoadingAvailablePlugins,
error: availablePluginsError error: availablePluginsError,
} = useAvailablePlugins({ enabled: !installed }); } = useAvailablePlugins({ enabled: !installed });
const { const {
data: installedPlugins, data: installedPlugins,
isLoading: isLoadingInstalledPlugins, isLoading: isLoadingInstalledPlugins,
error: installedPluginsError error: installedPluginsError,
} = useInstalledPlugins({ enabled: installed }); } = useInstalledPlugins({ enabled: installed });
const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins(); const { data: pendingPlugins, isLoading: isLoadingPendingPlugins, error: pendingPluginsError } = usePendingPlugins();
const pluginCenterAuthInfo = usePluginCenterAuthInfo(); const pluginCenterAuthInfo = usePluginCenterAuthInfo();
@@ -177,21 +177,31 @@ const PluginsOverview: FC<Props> = ({ installed }) => {
const computeUpdateAllSize = () => { const computeUpdateAllSize = () => {
const outdatedPlugins = collection?._embedded?.plugins.filter((p: Plugin) => p._links.update).length; const outdatedPlugins = collection?._embedded?.plugins.filter((p: Plugin) => p._links.update).length;
return t("plugins.outdatedPlugins", { return t("plugins.outdatedPlugins", {
count: outdatedPlugins count: outdatedPlugins,
}); });
}; };
const renderPluginsList = () => { const renderPluginsList = () => {
let pluginCenterStatusNotification: React.ReactNode;
if (collection && collection.pluginCenterStatus !== "OK") {
const type = collection.pluginCenterStatus === "DEACTIVATED" ? "info" : "danger";
pluginCenterStatusNotification = (
<Notification type={type}>{t(`plugins.pluginCenterStatus.${collection.pluginCenterStatus}`)}</Notification>
);
}
if (collection?._embedded && collection._embedded.plugins.length > 0) { if (collection?._embedded && collection._embedded.plugins.length > 0) {
return ( return (
<>
{pluginCenterStatusNotification}
<PluginsList <PluginsList
plugins={collection._embedded.plugins} plugins={collection._embedded.plugins}
openModal={setPluginModalContent} openModal={setPluginModalContent}
pluginCenterAuthInfo={pluginCenterAuthInfo.data} pluginCenterAuthInfo={pluginCenterAuthInfo.data}
/> />
</>
); );
} }
return <Notification type="info">{t("plugins.noPlugins")}</Notification>; return pluginCenterStatusNotification ?? <Notification type="info">{t("plugins.noPlugins")}</Notification>;
}; };
const renderModals = () => { const renderModals = () => {

View File

@@ -95,10 +95,10 @@ public class AvailablePluginResource {
@Produces(VndMediaType.PLUGIN_COLLECTION) @Produces(VndMediaType.PLUGIN_COLLECTION)
public Response getAvailablePlugins() { public Response getAvailablePlugins() {
PluginPermissions.read().check(); PluginPermissions.read().check();
List<InstalledPlugin> installed = pluginManager.getInstalled(); PluginManager.PluginResult plugins = pluginManager.getPlugins();
List<AvailablePlugin> available = pluginManager.getAvailable().stream().filter(a -> notInstalled(a, installed)).collect(Collectors.toList()); List<AvailablePlugin> 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<InstalledPlugin> installed) { private boolean notInstalled(AvailablePlugin a, List<InstalledPlugin> installed) {

View File

@@ -95,9 +95,8 @@ public class InstalledPluginResource {
) )
public Response getInstalledPlugins() { public Response getInstalledPlugins() {
PluginPermissions.read().check(); PluginPermissions.read().check();
List<InstalledPlugin> plugins = pluginManager.getInstalled(); PluginManager.PluginResult plugins = pluginManager.getPlugins();
List<AvailablePlugin> available = pluginManager.getAvailable(); return Response.ok(collectionMapper.mapInstalled(plugins)).build();
return Response.ok(collectionMapper.mapInstalled(plugins, available)).build();
} }
/** /**

View File

@@ -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;
}
}

View File

@@ -26,10 +26,9 @@ package sonia.scm.api.v2.resources;
import com.google.inject.Inject; import com.google.inject.Inject;
import de.otto.edison.hal.Embedded; import de.otto.edison.hal.Embedded;
import de.otto.edison.hal.HalRepresentation;
import de.otto.edison.hal.Links; import de.otto.edison.hal.Links;
import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.PluginCenterStatus;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import sonia.scm.plugin.PluginPermissions; import sonia.scm.plugin.PluginPermissions;
@@ -53,17 +52,18 @@ public class PluginDtoCollectionMapper {
this.manager = manager; this.manager = manager;
} }
public HalRepresentation mapInstalled(List<InstalledPlugin> plugins, List<AvailablePlugin> availablePlugins) { public PluginCollectionDto mapInstalled(PluginManager.PluginResult plugins) {
List<PluginDto> dtos = plugins List<PluginDto> dtos = plugins
.getInstalledPlugins()
.stream() .stream()
.map(i -> mapper.mapInstalled(i, availablePlugins)) .map(i -> mapper.mapInstalled(i, plugins.getAvailablePlugins()))
.collect(toList()); .collect(toList());
return new HalRepresentation(createInstalledPluginsLinks(), embedDtos(dtos)); return new PluginCollectionDto(createInstalledPluginsLinks(), embedDtos(dtos), plugins.getPluginCenterStatus());
} }
public HalRepresentation mapAvailable(List<AvailablePlugin> plugins) { public PluginCollectionDto mapAvailable(List<AvailablePlugin> plugins, PluginCenterStatus pluginCenterStatus) {
List<PluginDto> dtos = plugins.stream().map(mapper::mapAvailable).collect(toList()); List<PluginDto> 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() { private Links createInstalledPluginsLinks() {

View File

@@ -107,6 +107,17 @@ public class DefaultPluginManager implements PluginManager {
updateMayUninstallFlag(); updateMayUninstallFlag();
} }
@Override
public PluginResult getPlugins() {
PluginPermissions.read().check();
PluginCenterResult pluginCenterResult = center.getPluginResult();
return new PluginResult(
getInstalled(),
filterNotInstalledOrMoreUpToDate(pluginCenterResult.getPlugins()),
pluginCenterResult.getStatus()
);
}
@Override @Override
public Optional<AvailablePlugin> getAvailable(String name) { public Optional<AvailablePlugin> getAvailable(String name) {
PluginPermissions.read().check(); PluginPermissions.read().check();
@@ -144,7 +155,11 @@ public class DefaultPluginManager implements PluginManager {
@Override @Override
public List<AvailablePlugin> getAvailable() { public List<AvailablePlugin> getAvailable() {
PluginPermissions.read().check(); PluginPermissions.read().check();
return center.getAvailablePlugins() return filterNotInstalledOrMoreUpToDate(center.getAvailablePlugins());
}
private List<AvailablePlugin> filterNotInstalledOrMoreUpToDate(Set<AvailablePlugin> availablePlugins) {
return availablePlugins
.stream() .stream()
.filter(this::isNotInstalledOrMoreUpToDate) .filter(this::isNotInstalledOrMoreUpToDate)
.map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p)) .map(p -> getPending(p.getDescriptor().getInformation().getName()).orElse(p))

View File

@@ -32,6 +32,7 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.cache.Cache; import sonia.scm.cache.Cache;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent;
import sonia.scm.util.HttpUtil; import sonia.scm.util.HttpUtil;
import sonia.scm.util.SystemUtil; import sonia.scm.util.SystemUtil;
@@ -65,6 +66,12 @@ public class PluginCenter {
pluginCenterResultCache.clear(); pluginCenterResultCache.clear();
} }
@Subscribe
public void handle(ScmConfigurationChangedEvent event) {
LOG.debug("clear plugin center cache, because of {}", event);
pluginCenterResultCache.clear();
}
synchronized Set<AvailablePlugin> getAvailablePlugins() { synchronized Set<AvailablePlugin> getAvailablePlugins() {
String url = buildPluginUrl(configuration.getPluginUrl()); String url = buildPluginUrl(configuration.getPluginUrl());
return getPluginCenterResult(url).getPlugins(); return getPluginCenterResult(url).getPlugins();
@@ -75,6 +82,11 @@ public class PluginCenter {
return getPluginCenterResult(url).getPluginSets(); return getPluginCenterResult(url).getPluginSets();
} }
synchronized PluginCenterResult getPluginResult() {
String url = buildPluginUrl(configuration.getPluginUrl());
return getPluginCenterResult(url);
}
private PluginCenterResult getPluginCenterResult(String url) { private PluginCenterResult getPluginCenterResult(String url) {
PluginCenterResult pluginCenterResult = pluginCenterResultCache.get(url); PluginCenterResult pluginCenterResult = pluginCenterResultCache.get(url);
if (pluginCenterResult == null) { if (pluginCenterResult == null) {

View File

@@ -25,6 +25,7 @@
package sonia.scm.plugin; package sonia.scm.plugin;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import sonia.scm.event.ScmEventBus; import sonia.scm.event.ScmEventBus;
@@ -32,7 +33,6 @@ import sonia.scm.net.ahc.AdvancedHttpClient;
import sonia.scm.net.ahc.AdvancedHttpRequest; import sonia.scm.net.ahc.AdvancedHttpRequest;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.Collections;
import static sonia.scm.plugin.Tracing.SPAN_KIND; import static sonia.scm.plugin.Tracing.SPAN_KIND;
@@ -65,6 +65,10 @@ class PluginCenterLoader {
PluginCenterResult load(String url) { PluginCenterResult load(String url) {
try { 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); LOG.info("fetch plugins from {}", url);
AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND); AdvancedHttpRequest request = client.get(url).spanKind(SPAN_KIND);
if (authenticator.isAuthenticated()) { if (authenticator.isAuthenticated()) {
@@ -75,7 +79,7 @@ class PluginCenterLoader {
} catch (Exception ex) { } catch (Exception ex) {
LOG.error("failed to load plugins from plugin center, returning empty list", ex); LOG.error("failed to load plugins from plugin center, returning empty list", ex);
eventBus.post(new PluginCenterErrorEvent()); eventBus.post(new PluginCenterErrorEvent());
return new PluginCenterResult(Collections.emptySet(), Collections.emptySet()); return new PluginCenterResult(PluginCenterStatus.ERROR);
} }
} }
} }

View File

@@ -27,6 +27,7 @@ package sonia.scm.plugin;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import java.util.Collections;
import java.util.Set; import java.util.Set;
@AllArgsConstructor @AllArgsConstructor
@@ -34,4 +35,18 @@ import java.util.Set;
class PluginCenterResult { class PluginCenterResult {
private Set<AvailablePlugin> plugins; private Set<AvailablePlugin> plugins;
private Set<PluginSet> pluginSets; private Set<PluginSet> 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<AvailablePlugin> plugins, Set<PluginSet> pluginSets) {
this(plugins, pluginSets, PluginCenterStatus.OK);
}
} }

View File

@@ -24,7 +24,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
@@ -42,6 +41,7 @@ import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginCenterStatus;
import sonia.scm.plugin.PluginCondition; import sonia.scm.plugin.PluginCondition;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
@@ -116,9 +116,9 @@ class AvailablePluginResourceTest {
void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException { void getAvailablePlugins() throws URISyntaxException, UnsupportedEncodingException {
AvailablePlugin plugin = createAvailablePlugin(); AvailablePlugin plugin = createAvailablePlugin();
when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(plugin)); when(pluginManager.getPlugins()).thenReturn(new PluginManager.PluginResult(Collections.emptyList(), Collections.singletonList(plugin)));
when(pluginManager.getInstalled()).thenReturn(Collections.emptyList());
when(collectionMapper.mapAvailable(Collections.singletonList(plugin))).thenReturn(new MockedResultDto()); when(collectionMapper.mapAvailable(Collections.singletonList(plugin), PluginCenterStatus.OK)).thenReturn(new MockedResultDto());
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available");
request.accept(VndMediaType.PLUGIN_COLLECTION); request.accept(VndMediaType.PLUGIN_COLLECTION);
@@ -135,9 +135,8 @@ class AvailablePluginResourceTest {
AvailablePlugin availablePlugin = createAvailablePlugin(); AvailablePlugin availablePlugin = createAvailablePlugin();
InstalledPlugin installedPlugin = createInstalledPlugin(); InstalledPlugin installedPlugin = createInstalledPlugin();
when(pluginManager.getAvailable()).thenReturn(Collections.singletonList(availablePlugin)); when(pluginManager.getPlugins()).thenReturn(new PluginManager.PluginResult(Collections.singletonList(installedPlugin), Collections.singletonList(availablePlugin)));
when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); lenient().when(collectionMapper.mapAvailable(Collections.singletonList(availablePlugin), PluginCenterStatus.OK)).thenReturn(new MockedResultDto());
lenient().when(collectionMapper.mapAvailable(Collections.singletonList(availablePlugin))).thenReturn(new MockedResultDto());
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/available");
request.accept(VndMediaType.PLUGIN_COLLECTION); request.accept(VndMediaType.PLUGIN_COLLECTION);
@@ -261,7 +260,7 @@ class AvailablePluginResourceTest {
} }
} }
public class MockedResultDto extends HalRepresentation { public class MockedResultDto extends PluginCollectionDto {
public String getMarker() { public String getMarker() {
return "x"; return "x";
} }

View File

@@ -24,7 +24,6 @@
package sonia.scm.api.v2.resources; package sonia.scm.api.v2.resources;
import de.otto.edison.hal.HalRepresentation;
import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadContext;
@@ -110,8 +109,9 @@ class InstalledPluginResourceTest {
@Test @Test
void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException { void getInstalledPlugins() throws URISyntaxException, UnsupportedEncodingException {
InstalledPlugin installedPlugin = createInstalled(""); InstalledPlugin installedPlugin = createInstalled("");
when(pluginManager.getInstalled()).thenReturn(Collections.singletonList(installedPlugin)); PluginManager.PluginResult pluginResult = new PluginManager.PluginResult(Collections.singletonList(installedPlugin), emptyList());
when(collectionMapper.mapInstalled(Collections.singletonList(installedPlugin), Collections.emptyList())).thenReturn(new MockedResultDto()); when(pluginManager.getPlugins()).thenReturn(pluginResult);
when(collectionMapper.mapInstalled(pluginResult)).thenReturn(new MockedResultDto());
MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed"); MockHttpRequest request = MockHttpRequest.get("/v2/plugins/installed");
request.accept(VndMediaType.PLUGIN_COLLECTION); request.accept(VndMediaType.PLUGIN_COLLECTION);
@@ -184,7 +184,8 @@ class InstalledPluginResourceTest {
} }
} }
public class MockedResultDto extends HalRepresentation { public class MockedResultDto extends PluginCollectionDto {
public String getMarker() { public String getMarker() {
return "x"; return "x";
} }

View File

@@ -41,14 +41,15 @@ import sonia.scm.plugin.AvailablePlugin;
import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.AvailablePluginDescriptor;
import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.InstalledPlugin;
import sonia.scm.plugin.InstalledPluginDescriptor; import sonia.scm.plugin.InstalledPluginDescriptor;
import sonia.scm.plugin.PluginCenterStatus;
import sonia.scm.plugin.PluginInformation; import sonia.scm.plugin.PluginInformation;
import sonia.scm.plugin.PluginManager; import sonia.scm.plugin.PluginManager;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
@@ -88,14 +89,23 @@ class PluginDtoCollectionMapperTest {
ThreadContext.unbindSubject(); 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 @Test
void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() { void shouldMapInstalledPluginsWithoutUpdateWhenNoNewerVersionIsAvailable() {
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(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(createAvailablePlugin("scm-other-plugin", "2"))); singletonList(createAvailablePlugin("scm-other-plugin", "2"))));
List<HalRepresentation> plugins = result.getEmbedded().getItemsBy("plugins"); List<HalRepresentation> plugins = result.getEmbedded().getItemsBy("plugins");
assertThat(plugins).hasSize(1); assertThat(plugins).hasSize(1);
@@ -108,9 +118,9 @@ class PluginDtoCollectionMapperTest {
void shouldSetNewVersionInInstalledPluginWhenAvailableVersionIsNewer() { 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(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(createAvailablePlugin("scm-some-plugin", "2"))); singletonList(createAvailablePlugin("scm-some-plugin", "2"))));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.getVersion()).isEqualTo("1"); assertThat(plugin.getVersion()).isEqualTo("1");
@@ -122,9 +132,9 @@ class PluginDtoCollectionMapperTest {
when(subject.isPermitted("plugin:write")).thenReturn(false); when(subject.isPermitted("plugin:write")).thenReturn(false);
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(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(createAvailablePlugin("scm-some-plugin", "2"))); singletonList(createAvailablePlugin("scm-some-plugin", "2"))));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); assertThat(plugin.getLinks().getLinkBy("update")).isEmpty();
@@ -137,9 +147,9 @@ class PluginDtoCollectionMapperTest {
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
when(availablePlugin.isPending()).thenReturn(true); when(availablePlugin.isPending()).thenReturn(true);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(availablePlugin)); singletonList(availablePlugin)));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.getLinks().getLinkBy("update")).isEmpty(); assertThat(plugin.getLinks().getLinkBy("update")).isEmpty();
@@ -150,9 +160,9 @@ class PluginDtoCollectionMapperTest {
when(subject.isPermitted("plugin:write")).thenReturn(true); when(subject.isPermitted("plugin:write")).thenReturn(true);
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(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(createAvailablePlugin("scm-some-plugin", "2"))); singletonList(createAvailablePlugin("scm-some-plugin", "2"))));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty();
@@ -164,9 +174,9 @@ class PluginDtoCollectionMapperTest {
when(subject.isPermitted("plugin:write")).thenReturn(true); when(subject.isPermitted("plugin:write")).thenReturn(true);
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(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(createAvailablePlugin("scm-some-plugin", "2"))); singletonList(createAvailablePlugin("scm-some-plugin", "2"))));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty();
@@ -180,9 +190,9 @@ class PluginDtoCollectionMapperTest {
AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2"); AvailablePlugin availablePlugin = createAvailablePlugin("scm-some-plugin", "2");
when(availablePlugin.isPending()).thenReturn(true); when(availablePlugin.isPending()).thenReturn(true);
HalRepresentation result = mapper.mapInstalled( HalRepresentation result = mapper.mapInstalled(new PluginManager.PluginResult(
singletonList(createInstalledPlugin("scm-some-plugin", "1")), singletonList(createInstalledPlugin("scm-some-plugin", "1")),
singletonList(availablePlugin)); singletonList(availablePlugin)));
PluginDto plugin = getPluginDtoFromResult(result); PluginDto plugin = getPluginDtoFromResult(result);
assertThat(plugin.isPending()).isTrue(); assertThat(plugin.isPending()).isTrue();
@@ -223,4 +233,12 @@ class PluginDtoCollectionMapperTest {
lenient().when(plugin.getDescriptor()).thenReturn(descriptor); lenient().when(plugin.getDescriptor()).thenReturn(descriptor);
return plugin; return plugin;
} }
private static PluginManager.PluginResult emptyPluginResult(PluginCenterStatus status) {
return new PluginManager.PluginResult(
emptyList(),
emptyList(),
status
);
}
} }

View File

@@ -54,6 +54,7 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton; import static java.util.Collections.singleton;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -133,6 +134,27 @@ class DefaultPluginManagerTest {
ThreadContext.unbindSubject(); 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 @Test
void shouldReturnInstalledPlugins() { void shouldReturnInstalledPlugins() {
InstalledPlugin review = createInstalled("scm-review-plugin"); InstalledPlugin review = createInstalled("scm-review-plugin");
@@ -746,6 +768,7 @@ class DefaultPluginManagerTest {
assertThrows(AuthorizationException.class, () -> manager.getAvailable()); assertThrows(AuthorizationException.class, () -> manager.getAvailable());
assertThrows(AuthorizationException.class, () -> manager.getAvailable("test")); assertThrows(AuthorizationException.class, () -> manager.getAvailable("test"));
assertThrows(AuthorizationException.class, () -> manager.getPluginSets()); assertThrows(AuthorizationException.class, () -> manager.getPluginSets());
assertThrows(AuthorizationException.class, () -> manager.getPlugins());
} }
} }

View File

@@ -79,6 +79,7 @@ class PluginCenterLoaderTest {
PluginCenterResult fetched = loader.load(PLUGIN_URL); PluginCenterResult fetched = loader.load(PLUGIN_URL);
assertThat(fetched.getPlugins()).isSameAs(plugins); assertThat(fetched.getPlugins()).isSameAs(plugins);
assertThat(fetched.getPluginSets()).isSameAs(pluginSets); assertThat(fetched.getPluginSets()).isSameAs(pluginSets);
assertThat(fetched.getStatus()).isEqualTo(PluginCenterStatus.OK);
} }
private AdvancedHttpResponse request() throws IOException { private AdvancedHttpResponse request() throws IOException {
@@ -88,6 +89,14 @@ class PluginCenterLoaderTest {
return response; return response;
} }
@Test
void shouldReturnEmptySetIfPluginCenterIsDeactivated() {
PluginCenterResult fetch = loader.load("");
assertThat(fetch.getPlugins()).isEmpty();
assertThat(fetch.getPluginSets()).isEmpty();
assertThat(fetch.getStatus()).isSameAs(PluginCenterStatus.DEACTIVATED);
}
@Test @Test
void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException { void shouldReturnEmptySetIfPluginCenterNotBeReached() throws IOException {
when(client.get(PLUGIN_URL)).thenReturn(request); when(client.get(PLUGIN_URL)).thenReturn(request);
@@ -96,6 +105,7 @@ class PluginCenterLoaderTest {
PluginCenterResult fetch = loader.load(PLUGIN_URL); PluginCenterResult fetch = loader.load(PLUGIN_URL);
assertThat(fetch.getPlugins()).isEmpty(); assertThat(fetch.getPlugins()).isEmpty();
assertThat(fetch.getPluginSets()).isEmpty(); assertThat(fetch.getPluginSets()).isEmpty();
assertThat(fetch.getStatus()).isSameAs(PluginCenterStatus.ERROR);
} }
@Test @Test

View File

@@ -33,8 +33,8 @@ import sonia.scm.SCMContextProvider;
import sonia.scm.cache.CacheManager; import sonia.scm.cache.CacheManager;
import sonia.scm.cache.MapCacheManager; import sonia.scm.cache.MapCacheManager;
import sonia.scm.config.ScmConfiguration; import sonia.scm.config.ScmConfiguration;
import sonia.scm.config.ScmConfigurationChangedEvent;
import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
@@ -92,7 +92,7 @@ class PluginCenterTest {
Set<PluginSet> pluginSets = new HashSet<>(); Set<PluginSet> pluginSets = new HashSet<>();
PluginCenterResult first = new PluginCenterResult(plugins, pluginSets); 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);
assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins); assertThat(pluginCenter.getAvailablePlugins()).isSameAs(plugins);
@@ -101,12 +101,12 @@ class PluginCenterTest {
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
void shouldClearCache() { void shouldClearCacheOnPluginCenterLogin() {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();
Set<PluginSet> pluginSets = new HashSet<>(); Set<PluginSet> pluginSets = new HashSet<>();
PluginCenterResult first = new PluginCenterResult(plugins, pluginSets); 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);
assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets); assertThat(pluginCenter.getAvailablePluginSets()).isSameAs(pluginSets);
@@ -115,6 +115,22 @@ class PluginCenterTest {
assertThat(pluginCenter.getAvailablePluginSets()).isNotSameAs(pluginSets); assertThat(pluginCenter.getAvailablePluginSets()).isNotSameAs(pluginSets);
} }
@Test
@SuppressWarnings("unchecked")
void shouldClearCacheOnConfigChange() {
Set<AvailablePlugin> plugins = new HashSet<>();
Set<PluginSet> 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 @Test
void shouldLoadOnRefresh() { void shouldLoadOnRefresh() {
Set<AvailablePlugin> plugins = new HashSet<>(); Set<AvailablePlugin> plugins = new HashSet<>();