mirror of https://github.com/halo-dev/halo
feat: add reverse proxy router registry for plugin by reverse proxy rules (#2151)
* feat: add reverse proxy router registry for plugin by reverse proxy rules * Update src/main/java/run/halo/app/plugin/resources/ReverseProxyRouterFunctionFactory.java Co-authored-by: John Niang <johnniang@fastmail.com> * refactor: merge stream operation * refactor: plugin composite router function * feat: add unit test case Co-authored-by: John Niang <johnniang@fastmail.com>pull/2158/head
parent
a2f49c60bc
commit
6bbaa9aeba
|
@ -0,0 +1,47 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import lombok.experimental.UtilityClass;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* Path manipulation tool class.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@UtilityClass
|
||||
public class PathUtils {
|
||||
|
||||
/**
|
||||
* Combine paths based on the passed in path segments parameters.
|
||||
*
|
||||
* @param pathSegments Path segments to be combined
|
||||
* @return the combined path
|
||||
*/
|
||||
public static String combinePath(String... pathSegments) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String path : pathSegments) {
|
||||
String s = path.startsWith("/") ? path : "/" + path;
|
||||
String segment = s.endsWith("/") ? s.substring(0, s.length() - 1) : s;
|
||||
sb.append(segment);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* <p>Append a {@code '/'} if the path does not end with a {@code '/'}.</p>
|
||||
* Examples are as follows:
|
||||
* <pre>
|
||||
* PathUtils.appendPathSeparatorIfMissing("hello") -> hello/
|
||||
* PathUtils.appendPathSeparatorIfMissing("some-path/") -> some-path/
|
||||
* PathUtils.appendPathSeparatorIfMissing(null) -> null
|
||||
* </pre>
|
||||
*
|
||||
* @param path a path
|
||||
* @return A new String if suffix was appended, the same string otherwise.
|
||||
*/
|
||||
public static String appendPathSeparatorIfMissing(String path) {
|
||||
return StringUtils.appendIfMissing(path, "/", "/");
|
||||
}
|
||||
}
|
|
@ -24,6 +24,7 @@ import org.springframework.beans.factory.InitializingBean;
|
|||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.lang.NonNull;
|
||||
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStateChangedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
|
@ -367,5 +368,12 @@ public class HaloPluginManager extends DefaultPluginManager
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PluginWrapper loadPluginFromPath(Path pluginPath) {
|
||||
PluginWrapper pluginWrapper = super.loadPluginFromPath(pluginPath);
|
||||
rootApplicationContext.publishEvent(new HaloPluginLoadedEvent(this, pluginWrapper));
|
||||
return pluginWrapper;
|
||||
}
|
||||
|
||||
// end-region
|
||||
}
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
||||
|
||||
/**
|
||||
* A composite {@link RouterFunction} implementation for plugin.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PluginCompositeRouterFunction implements RouterFunction<ServerResponse> {
|
||||
|
||||
private final Map<String, RouterFunction<ServerResponse>> routerFunctionRegistry =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
private final ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
|
||||
public PluginCompositeRouterFunction(
|
||||
ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory) {
|
||||
this.reverseProxyRouterFunctionFactory = reverseProxyRouterFunctionFactory;
|
||||
}
|
||||
|
||||
public RouterFunction<ServerResponse> getRouterFunction(String pluginId) {
|
||||
return routerFunctionRegistry.get(pluginId);
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(routerFunctionRegistry.values())
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.next();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(@NonNull RouterFunctions.Visitor visitor) {
|
||||
routerFunctionRegistry.values().forEach(routerFunction -> routerFunction.accept(visitor));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the user-defined {@link RouterFunction} from the plugin
|
||||
* {@link PluginApplicationContext} and create {@link RouterFunction} according to the
|
||||
* reverse proxy configuration file then register them to {@link #routerFunctionRegistry}.
|
||||
*
|
||||
* @param haloPluginStartedEvent event for plugin started
|
||||
*/
|
||||
@EventListener(HaloPluginStartedEvent.class)
|
||||
public void onPluginStarted(HaloPluginStartedEvent haloPluginStartedEvent) {
|
||||
PluginWrapper plugin = haloPluginStartedEvent.getPlugin();
|
||||
// Obtain plugin application context
|
||||
PluginApplicationContext pluginApplicationContext =
|
||||
ExtensionContextRegistry.getInstance().getByPluginId(plugin.getPluginId());
|
||||
|
||||
// create reverse proxy router function for plugin
|
||||
RouterFunction<ServerResponse> reverseProxyRouterFunction =
|
||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
||||
|
||||
routerFunctions(pluginApplicationContext)
|
||||
.stream()
|
||||
.reduce(RouterFunction::and)
|
||||
.map(compositeRouterFunction -> {
|
||||
if (reverseProxyRouterFunction != null) {
|
||||
compositeRouterFunction.andOther(reverseProxyRouterFunction);
|
||||
}
|
||||
return compositeRouterFunction;
|
||||
})
|
||||
.ifPresent(routerFunction -> {
|
||||
routerFunctionRegistry.put(plugin.getPluginId(), routerFunction);
|
||||
});
|
||||
}
|
||||
|
||||
@EventListener(HaloPluginStoppedEvent.class)
|
||||
public void onPluginStopped(HaloPluginStoppedEvent haloPluginStoppedEvent) {
|
||||
PluginWrapper plugin = haloPluginStoppedEvent.getPlugin();
|
||||
routerFunctionRegistry.remove(plugin.getPluginId());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<RouterFunction<ServerResponse>> routerFunctions(
|
||||
PluginApplicationContext applicationContext) {
|
||||
List<RouterFunction<ServerResponse>> functions =
|
||||
applicationContext.getBeanProvider(RouterFunction.class)
|
||||
.orderedStream()
|
||||
.map(router -> (RouterFunction<ServerResponse>) router)
|
||||
.collect(Collectors.toList());
|
||||
return (!CollectionUtils.isEmpty(functions) ? functions : Collections.emptyList());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Schemes;
|
||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
|
||||
import run.halo.app.plugin.resources.ReverseProxy;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Component
|
||||
public class PluginLoadedListener implements ApplicationListener<HaloPluginLoadedEvent> {
|
||||
private static final String REVERSE_PROXY_NAME = "extensions/reverseProxy.yaml";
|
||||
private final ExtensionClient extensionClient;
|
||||
|
||||
public PluginLoadedListener(ExtensionClient extensionClient) {
|
||||
this.extensionClient = extensionClient;
|
||||
|
||||
// TODO Optimize schemes register
|
||||
Schemes.INSTANCE.register(Plugin.class);
|
||||
Schemes.INSTANCE.register(ReverseProxy.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(HaloPluginLoadedEvent event) {
|
||||
PluginWrapper pluginWrapper = event.getPluginWrapper();
|
||||
// TODO: Optimize plugin custom resource loading method
|
||||
// load plugin.yaml
|
||||
YamlPluginFinder yamlPluginFinder = new YamlPluginFinder();
|
||||
Plugin plugin = yamlPluginFinder.find(pluginWrapper.getPluginPath());
|
||||
DefaultResourceLoader defaultResourceLoader =
|
||||
new DefaultResourceLoader(pluginWrapper.getPluginClassLoader());
|
||||
extensionClient.create(plugin);
|
||||
// load reverse proxy
|
||||
Resource resource = defaultResourceLoader.getResource(REVERSE_PROXY_NAME);
|
||||
if (resource.exists()) {
|
||||
YamlUnstructuredLoader unstructuredLoader = new YamlUnstructuredLoader(resource);
|
||||
unstructuredLoader.load().forEach(extensionClient::create);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package run.halo.app.plugin.event;
|
||||
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class HaloPluginLoadedEvent extends ApplicationEvent {
|
||||
private final PluginWrapper pluginWrapper;
|
||||
|
||||
|
||||
public HaloPluginLoadedEvent(Object source, PluginWrapper pluginWrapper) {
|
||||
super(source);
|
||||
this.pluginWrapper = pluginWrapper;
|
||||
}
|
||||
|
||||
public PluginWrapper getPluginWrapper() {
|
||||
return pluginWrapper;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package run.halo.app.plugin.resources;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
|
||||
/**
|
||||
* <p>The reverse proxy custom resource is used to configure a path to proxy it to a directory or
|
||||
* file.</p>
|
||||
* <p>HTTP proxy may be added in the future.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@GVK(group = "plugin.halo.run", kind = "ReverseProxy", version = "v1alpha1",
|
||||
plural = "reverseproxies", singular = "reverseproxy")
|
||||
public class ReverseProxy extends AbstractExtension {
|
||||
private List<ReverseProxyRule> rules;
|
||||
|
||||
record ReverseProxyRule(String path, FileReverseProxyProvider file) {
|
||||
}
|
||||
|
||||
record FileReverseProxyProvider(String directory, String filename) {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
package run.halo.app.plugin.resources;
|
||||
|
||||
import static org.springframework.http.MediaType.ALL;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.infra.utils.PathUtils;
|
||||
import run.halo.app.plugin.PluginApplicationContext;
|
||||
import run.halo.app.plugin.resources.ReverseProxy.FileReverseProxyProvider;
|
||||
import run.halo.app.plugin.resources.ReverseProxy.ReverseProxyRule;
|
||||
|
||||
/**
|
||||
* <p>Plugin's reverse proxy router factory.</p>
|
||||
* <p>It creates a {@link RouterFunction} based on the ReverseProxy rule configured by
|
||||
* the plugin.</p>
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class ReverseProxyRouterFunctionFactory {
|
||||
private static final String REVERSE_PROXY_API_PREFIX = "/assets";
|
||||
|
||||
public static final String REVERSE_PROXY_PLUGIN_LABEL_NAME = "plugin.halo.run/plugin-name";
|
||||
|
||||
private final ExtensionClient extensionClient;
|
||||
|
||||
public ReverseProxyRouterFunctionFactory(ExtensionClient extensionClient) {
|
||||
this.extensionClient = extensionClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Create {@link RouterFunction} according to the {@link ReverseProxy} custom resource
|
||||
* configuration of the plugin.</p>
|
||||
* <p>Note that: returns {@code Null} if the plugin does not have a {@link ReverseProxy} custom
|
||||
* resource.</p>
|
||||
*
|
||||
* @param pluginApplicationContext plugin application context
|
||||
* @return A reverse proxy RouterFunction handle(nullable)
|
||||
*/
|
||||
@Nullable
|
||||
public RouterFunction<ServerResponse> create(
|
||||
PluginApplicationContext pluginApplicationContext) {
|
||||
String pluginId = pluginApplicationContext.getPluginId();
|
||||
List<ReverseProxyRule> reverseProxyRules = getReverseProxyRules(pluginId);
|
||||
return createReverseProxyRouterFunction(reverseProxyRules, pluginApplicationContext);
|
||||
}
|
||||
|
||||
private RouterFunction<ServerResponse> createReverseProxyRouterFunction(
|
||||
List<ReverseProxyRule> rules, PluginApplicationContext pluginApplicationContext) {
|
||||
Assert.notNull(rules, "The reverseProxyRules must not be null.");
|
||||
Assert.notNull(pluginApplicationContext, "The pluginApplicationContext must not be null.");
|
||||
|
||||
String pluginId = pluginApplicationContext.getPluginId();
|
||||
return rules.stream()
|
||||
.map(rule -> {
|
||||
String routePath = buildRoutePath(pluginId, rule);
|
||||
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginId,
|
||||
routePath);
|
||||
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
|
||||
request -> {
|
||||
Resource resource =
|
||||
loadResourceByFileRule(pluginApplicationContext, rule, request);
|
||||
if (!resource.exists()) {
|
||||
return ServerResponse.notFound().build();
|
||||
}
|
||||
return ServerResponse.ok()
|
||||
.bodyValue(resource);
|
||||
});
|
||||
})
|
||||
.reduce(RouterFunction::and)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<ReverseProxyRule> getReverseProxyRules(String pluginId) {
|
||||
return extensionClient.list(ReverseProxy.class,
|
||||
reverseProxy -> {
|
||||
String pluginName = reverseProxy.getMetadata()
|
||||
.getLabels()
|
||||
.get(REVERSE_PROXY_PLUGIN_LABEL_NAME);
|
||||
return pluginId.equals(pluginName);
|
||||
},
|
||||
null)
|
||||
.stream()
|
||||
.map(ReverseProxy::getRules)
|
||||
.flatMap(List::stream)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private String buildRoutePath(String pluginId, ReverseProxyRule reverseProxyRule) {
|
||||
return PathUtils.combinePath(REVERSE_PROXY_API_PREFIX, pluginId, reverseProxyRule.path());
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>File load rule: if the directory is configured but the file name is not configured, it
|
||||
* means access through wildcards. Otherwise, if only the file name is configured, this
|
||||
* method only returns the file pointed to by the rule.</p>
|
||||
* <p>You should only use {@link Resource#getInputStream()} to get resource content instead of
|
||||
* {@link Resource#getFile()},the resource is loaded from the plugin jar file using a
|
||||
* specific plugin class loader; if you use {@link Resource#getFile()}, you cannot get the
|
||||
* file.</p>
|
||||
* <p>Note that a returned Resource handle does not imply an existing resource; you need to
|
||||
* invoke {@link Resource#exists()} to check for existence</p>
|
||||
*
|
||||
* @param pluginApplicationContext load file from plugin
|
||||
* @param rule reverse proxy rule
|
||||
* @param request client request
|
||||
* @return a Resource handle for the specified resource location by the plugin(never null);
|
||||
*/
|
||||
@NonNull
|
||||
private Resource loadResourceByFileRule(PluginApplicationContext pluginApplicationContext,
|
||||
ReverseProxyRule rule, ServerRequest request) {
|
||||
Assert.notNull(rule.file(), "File rule must not be null.");
|
||||
FileReverseProxyProvider file = rule.file();
|
||||
String directory = file.directory();
|
||||
|
||||
// Decision file name
|
||||
String filename;
|
||||
String configuredFilename = file.filename();
|
||||
if (StringUtils.isNotBlank(configuredFilename)) {
|
||||
filename = configuredFilename;
|
||||
} else {
|
||||
AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||
String routePath = buildRoutePath(pluginApplicationContext.getPluginId(), rule);
|
||||
filename =
|
||||
antPathMatcher.extractPathWithinPattern(routePath, request.path());
|
||||
}
|
||||
|
||||
String filePath = PathUtils.appendPathSeparatorIfMissing(directory) + filename;
|
||||
return pluginApplicationContext.getResource(filePath);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Tests for {@link PathUtils}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
class PathUtilsTest {
|
||||
|
||||
@Test
|
||||
void combinePath() {
|
||||
Map<String, String> combinePathCases = getCombinePathCases();
|
||||
combinePathCases.forEach((segments, expected) -> {
|
||||
String s = PathUtils.combinePath(segments.split(","));
|
||||
assertThat(s).isEqualTo(expected);
|
||||
});
|
||||
}
|
||||
|
||||
private Map<String, String> getCombinePathCases() {
|
||||
Map<String, String> combinePathCases = new HashMap<>();
|
||||
combinePathCases.put("a,b,c", "/a/b/c");
|
||||
combinePathCases.put("/a,b,c", "/a/b/c");
|
||||
combinePathCases.put("/a,b/,c", "/a/b/c");
|
||||
combinePathCases.put("/a,/b/,c", "/a/b/c");
|
||||
return combinePathCases;
|
||||
}
|
||||
|
||||
@Test
|
||||
void appendPathSeparatorIfMissing() {
|
||||
String s = PathUtils.appendPathSeparatorIfMissing("a");
|
||||
assertThat(s).isEqualTo("a/");
|
||||
|
||||
s = PathUtils.appendPathSeparatorIfMissing("a/");
|
||||
assertThat(s).isEqualTo("a/");
|
||||
|
||||
s = PathUtils.appendPathSeparatorIfMissing(null);
|
||||
assertThat(s).isEqualTo(null);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
package run.halo.app.plugin;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.pf4j.PluginWrapper;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.reactive.function.server.support.RouterFunctionMapping;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||
import run.halo.app.plugin.event.HaloPluginStoppedEvent;
|
||||
import run.halo.app.plugin.resources.ReverseProxyRouterFunctionFactory;
|
||||
|
||||
/**
|
||||
* Tests for {@link PluginCompositeRouterFunction}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginCompositeRouterFunctionTest {
|
||||
private final ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
|
||||
|
||||
@Mock
|
||||
private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
|
||||
@Mock
|
||||
private PluginApplicationContext pluginApplicationContext;
|
||||
|
||||
@Mock
|
||||
private PluginWrapper pluginWrapper;
|
||||
|
||||
private PluginCompositeRouterFunction compositeRouterFunction;
|
||||
|
||||
private HandlerFunction<ServerResponse> handlerFunction;
|
||||
private RouterFunction<ServerResponse> routerFunction;
|
||||
|
||||
@BeforeEach
|
||||
@SuppressWarnings("unchecked")
|
||||
void setUp() {
|
||||
compositeRouterFunction =
|
||||
new PluginCompositeRouterFunction(reverseProxyRouterFunctionFactory);
|
||||
|
||||
ExtensionContextRegistry.getInstance().register("fakeA", pluginApplicationContext);
|
||||
when(pluginWrapper.getPluginId()).thenReturn("fakeA");
|
||||
|
||||
handlerFunction = request -> ServerResponse.ok().build();
|
||||
routerFunction = request -> Mono.just(handlerFunction);
|
||||
|
||||
ObjectProvider objectProvider = mock(ObjectProvider.class);
|
||||
when(objectProvider.orderedStream()).thenReturn(Stream.of(routerFunction));
|
||||
|
||||
when(pluginApplicationContext.getBeanProvider(RouterFunction.class))
|
||||
.thenReturn(objectProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
void route() {
|
||||
// trigger haloPluginStartedEvent
|
||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
|
||||
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
||||
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
||||
|
||||
Mono<Object> result = mapping.getHandler(createExchange("https://example.com/match"));
|
||||
|
||||
StepVerifier.create(result)
|
||||
.expectNext(handlerFunction)
|
||||
.expectComplete()
|
||||
.verify();
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPluginStarted() {
|
||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
||||
|
||||
// trigger haloPluginStartedEvent
|
||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
||||
}
|
||||
|
||||
@Test
|
||||
void onPluginStopped() {
|
||||
// trigger haloPluginStartedEvent
|
||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
||||
|
||||
// trigger HaloPluginStoppedEvent
|
||||
compositeRouterFunction.onPluginStopped(new HaloPluginStoppedEvent(this, pluginWrapper));
|
||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
||||
}
|
||||
|
||||
private ServerWebExchange createExchange(String urlTemplate) {
|
||||
return MockServerWebExchange.from(MockServerHttpRequest.get(urlTemplate));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
package run.halo.app.plugin.resources;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.plugin.PluginApplicationContext;
|
||||
|
||||
/**
|
||||
* Tests for {@link ReverseProxyRouterFunctionFactory}.
|
||||
*
|
||||
* @author guqing
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class ReverseProxyRouterFunctionFactoryTest {
|
||||
|
||||
@Mock
|
||||
private ExtensionClient extensionClient;
|
||||
|
||||
@Mock
|
||||
private PluginApplicationContext pluginApplicationContext;
|
||||
|
||||
private ReverseProxyRouterFunctionFactory reverseProxyRouterFunctionFactory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
reverseProxyRouterFunctionFactory = new ReverseProxyRouterFunctionFactory(extensionClient);
|
||||
|
||||
ReverseProxy reverseProxy = mockReverseProxy();
|
||||
|
||||
when(pluginApplicationContext.getPluginId()).thenReturn("fakeA");
|
||||
when(extensionClient.list(eq(ReverseProxy.class), any(), any())).thenReturn(
|
||||
List.of(reverseProxy));
|
||||
}
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
RouterFunction<ServerResponse> routerFunction =
|
||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
||||
assertThat(routerFunction).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
private ReverseProxy mockReverseProxy() {
|
||||
ReverseProxy.ReverseProxyRule reverseProxyRule =
|
||||
new ReverseProxy.ReverseProxyRule("/static/**",
|
||||
new ReverseProxy.FileReverseProxyProvider("static", ""));
|
||||
ReverseProxy reverseProxy = new ReverseProxy();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setLabels(
|
||||
Map.of(ReverseProxyRouterFunctionFactory.REVERSE_PROXY_PLUGIN_LABEL_NAME, "fakeA"));
|
||||
reverseProxy.setMetadata(metadata);
|
||||
reverseProxy.setRules(List.of(reverseProxyRule));
|
||||
return reverseProxy;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue