mirror of https://github.com/halo-dev/halo
Replace JDBC to R2DBC (#2324)
#### What type of PR is this? /kind feature /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 1. Replace JDBC to R2DBC 2. Make our system fully reactive #### Which issue(s) this PR fixes Fixes #2308 #### Special notes for your reviewer: #### Does this PR introduce a user-facing change? ```release-note None ```pull/2333/head
parent
84eef54603
commit
9911ba927d
14
build.gradle
14
build.gradle
|
@ -26,11 +26,6 @@ repositories {
|
||||||
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
implementation {
|
|
||||||
exclude module: "spring-boot-starter-tomcat"
|
|
||||||
exclude module: "slf4j-log4j12"
|
|
||||||
exclude module: 'junit'
|
|
||||||
}
|
|
||||||
compileOnly {
|
compileOnly {
|
||||||
extendsFrom annotationProcessor
|
extendsFrom annotationProcessor
|
||||||
}
|
}
|
||||||
|
@ -57,6 +52,7 @@ dependencies {
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||||
|
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
|
||||||
|
|
||||||
// Spring Security
|
// Spring Security
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
|
@ -80,9 +76,15 @@ dependencies {
|
||||||
|
|
||||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||||
|
|
||||||
runtimeOnly "com.h2database:h2"
|
// R2DBC
|
||||||
|
// Currently, official doesn't support mssql and mariadb yet until drivers are available.
|
||||||
|
// See https://github.com/spring-projects/spring-data-relational/commit/ee6c2c89b5c433748b22a79cf40dc8e01142caa3
|
||||||
|
// for more.
|
||||||
|
runtimeOnly 'io.r2dbc:r2dbc-h2'
|
||||||
|
runtimeOnly 'com.h2database:h2'
|
||||||
runtimeOnly 'mysql:mysql-connector-java'
|
runtimeOnly 'mysql:mysql-connector-java'
|
||||||
runtimeOnly 'org.postgresql:postgresql'
|
runtimeOnly 'org.postgresql:postgresql'
|
||||||
|
runtimeOnly 'org.postgresql:r2dbc-postgresql'
|
||||||
|
|
||||||
annotationProcessor 'org.projectlombok:lombok'
|
annotationProcessor 'org.projectlombok:lombok'
|
||||||
testAnnotationProcessor 'org.projectlombok:lombok'
|
testAnnotationProcessor 'org.projectlombok:lombok'
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.config;
|
package run.halo.app.config;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
@ -21,19 +20,16 @@ import run.halo.app.core.extension.reconciler.RoleReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.ThemeReconciler;
|
import run.halo.app.core.extension.reconciler.ThemeReconciler;
|
||||||
import run.halo.app.core.extension.reconciler.UserReconciler;
|
import run.halo.app.core.extension.reconciler.UserReconciler;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.DefaultExtensionClient;
|
|
||||||
import run.halo.app.extension.DefaultSchemeManager;
|
import run.halo.app.extension.DefaultSchemeManager;
|
||||||
import run.halo.app.extension.DefaultSchemeWatcherManager;
|
import run.halo.app.extension.DefaultSchemeWatcherManager;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.JSONExtensionConverter;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.SchemeManager;
|
import run.halo.app.extension.SchemeManager;
|
||||||
import run.halo.app.extension.SchemeWatcherManager;
|
import run.halo.app.extension.SchemeWatcherManager;
|
||||||
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
|
||||||
import run.halo.app.extension.controller.Controller;
|
import run.halo.app.extension.controller.Controller;
|
||||||
import run.halo.app.extension.controller.ControllerBuilder;
|
import run.halo.app.extension.controller.ControllerBuilder;
|
||||||
import run.halo.app.extension.controller.ControllerManager;
|
import run.halo.app.extension.controller.ControllerManager;
|
||||||
import run.halo.app.extension.router.ExtensionCompositeRouterFunction;
|
import run.halo.app.extension.router.ExtensionCompositeRouterFunction;
|
||||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||||
|
@ -42,19 +38,13 @@ import run.halo.app.plugin.resources.JsBundleRuleProvider;
|
||||||
public class ExtensionConfiguration {
|
public class ExtensionConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
RouterFunction<ServerResponse> extensionsRouterFunction(ExtensionClient client,
|
RouterFunction<ServerResponse> extensionsRouterFunction(ReactiveExtensionClient client,
|
||||||
SchemeWatcherManager watcherManager) {
|
SchemeWatcherManager watcherManager) {
|
||||||
return new ExtensionCompositeRouterFunction(client, watcherManager);
|
return new ExtensionCompositeRouterFunction(client, watcherManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
ExtensionClient extensionClient(ExtensionStoreClient storeClient, SchemeManager schemeManager) {
|
SchemeManager schemeManager(SchemeWatcherManager watcherManager) {
|
||||||
var converter = new JSONExtensionConverter(schemeManager);
|
|
||||||
return new DefaultExtensionClient(storeClient, converter, schemeManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
|
||||||
SchemeManager schemeManager(SchemeWatcherManager watcherManager, List<SchemeWatcher> watchers) {
|
|
||||||
return new DefaultSchemeManager(watcherManager);
|
return new DefaultSchemeManager(watcherManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.core.extension.service.UserService;
|
import run.halo.app.core.extension.service.UserService;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.properties.JwtProperties;
|
import run.halo.app.infra.properties.JwtProperties;
|
||||||
import run.halo.app.security.DefaultUserDetailService;
|
import run.halo.app.security.DefaultUserDetailService;
|
||||||
|
@ -135,9 +135,9 @@ public class WebServerSecurityConfig {
|
||||||
@ConditionalOnProperty(name = "halo.security.initializer.disabled",
|
@ConditionalOnProperty(name = "halo.security.initializer.disabled",
|
||||||
havingValue = "false",
|
havingValue = "false",
|
||||||
matchIfMissing = true)
|
matchIfMissing = true)
|
||||||
SuperAdminInitializer superAdminInitializer(ExtensionClient client, HaloProperties halo) {
|
SuperAdminInitializer superAdminInitializer(ReactiveExtensionClient client,
|
||||||
return new SuperAdminInitializer(client,
|
HaloProperties halo) {
|
||||||
passwordEncoder(),
|
return new SuperAdminInitializer(client, passwordEncoder(),
|
||||||
halo.getSecurity().getInitializer());
|
halo.getSecurity().getInitializer());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.plugin.PluginProperties;
|
import run.halo.app.plugin.PluginProperties;
|
||||||
import run.halo.app.plugin.YamlPluginFinder;
|
import run.halo.app.plugin.YamlPluginFinder;
|
||||||
|
|
||||||
|
@ -35,9 +35,9 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private final PluginProperties pluginProperties;
|
private final PluginProperties pluginProperties;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public PluginEndpoint(PluginProperties pluginProperties, ExtensionClient client) {
|
public PluginEndpoint(PluginProperties pluginProperties, ReactiveExtensionClient client) {
|
||||||
this.pluginProperties = pluginProperties;
|
this.pluginProperties = pluginProperties;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -74,19 +74,16 @@ public class PluginEndpoint implements CustomEndpoint {
|
||||||
createDirectoriesIfNotExists(pluginRoot);
|
createDirectoriesIfNotExists(pluginRoot);
|
||||||
var pluginPath = pluginRoot.resolve(file.filename());
|
var pluginPath = pluginRoot.resolve(file.filename());
|
||||||
return file.transferTo(pluginPath).thenReturn(pluginPath);
|
return file.transferTo(pluginPath).thenReturn(pluginPath);
|
||||||
}).map(pluginPath -> {
|
})
|
||||||
|
.flatMap(pluginPath -> {
|
||||||
log.info("Plugin uploaded at {}", pluginPath);
|
log.info("Plugin uploaded at {}", pluginPath);
|
||||||
var plugin = new YamlPluginFinder().find(pluginPath);
|
var plugin = new YamlPluginFinder().find(pluginPath);
|
||||||
// overwrite the enabled flag
|
// overwrite the enabled flag
|
||||||
plugin.getSpec().setEnabled(false);
|
plugin.getSpec().setEnabled(false);
|
||||||
var createdPlugin =
|
return client.fetch(Plugin.class, plugin.getMetadata().getName())
|
||||||
client.fetch(Plugin.class, plugin.getMetadata().getName()).orElseGet(() -> {
|
.switchIfEmpty(Mono.defer(() -> client.create(plugin)));
|
||||||
client.create(plugin);
|
})
|
||||||
return client.fetch(Plugin.class, plugin.getMetadata().getName())
|
.flatMap(plugin -> ServerResponse.ok()
|
||||||
.orElseThrow();
|
|
||||||
});
|
|
||||||
return createdPlugin;
|
|
||||||
}).flatMap(plugin -> ServerResponse.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(plugin));
|
.bodyValue(plugin));
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import java.util.zip.ZipInputStream;
|
import java.util.zip.ZipInputStream;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
@ -36,11 +37,12 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Setting;
|
import run.halo.app.core.extension.Setting;
|
||||||
import run.halo.app.core.extension.Theme;
|
import run.halo.app.core.extension.Theme;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.exception.ThemeInstallationException;
|
import run.halo.app.infra.exception.ThemeInstallationException;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
|
@ -56,10 +58,10 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
@Component
|
@Component
|
||||||
public class ThemeEndpoint implements CustomEndpoint {
|
public class ThemeEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
public ThemeEndpoint(ExtensionClient client, HaloProperties haloProperties) {
|
public ThemeEndpoint(ReactiveExtensionClient client, HaloProperties haloProperties) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
}
|
}
|
||||||
|
@ -97,7 +99,7 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
.map(DataBuffer::asInputStream)
|
.map(DataBuffer::asInputStream)
|
||||||
.reduce(SequenceInputStream::new)
|
.reduce(SequenceInputStream::new)
|
||||||
.map(inputStream -> ThemeUtils.unzipThemeTo(inputStream, getThemeWorkDir())))
|
.map(inputStream -> ThemeUtils.unzipThemeTo(inputStream, getThemeWorkDir())))
|
||||||
.map(this::persistent)
|
.flatMap(this::persistent)
|
||||||
.flatMap(theme -> ServerResponse.ok()
|
.flatMap(theme -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(theme));
|
.bodyValue(theme));
|
||||||
|
@ -112,49 +114,58 @@ public class ThemeEndpoint implements CustomEndpoint {
|
||||||
* @return a theme custom model
|
* @return a theme custom model
|
||||||
* @see Theme
|
* @see Theme
|
||||||
*/
|
*/
|
||||||
public Theme persistent(Unstructured themeManifest) {
|
public Mono<Theme> persistent(Unstructured themeManifest) {
|
||||||
Assert.state(StringUtils.equals(Theme.KIND, themeManifest.getKind()),
|
Assert.state(StringUtils.equals(Theme.KIND, themeManifest.getKind()),
|
||||||
"Theme manifest kind must be Theme.");
|
"Theme manifest kind must be Theme.");
|
||||||
client.create(themeManifest);
|
return client.create(themeManifest)
|
||||||
Theme theme = client.fetch(Theme.class, themeManifest.getMetadata().getName())
|
.map(theme -> Unstructured.OBJECT_MAPPER.convertValue(theme, Theme.class))
|
||||||
.orElseThrow();
|
.flatMap(theme -> {
|
||||||
List<Unstructured> unstructureds = ThemeUtils.loadThemeResources(getThemePath(theme));
|
var unstructureds = ThemeUtils.loadThemeResources(getThemePath(theme));
|
||||||
if (unstructureds.stream()
|
if (unstructureds.stream()
|
||||||
.filter(unstructured -> unstructured.getKind().equals(Setting.KIND))
|
.filter(hasSettingsYaml(theme))
|
||||||
.filter(unstructured -> unstructured.getMetadata().getName()
|
.count() > 1) {
|
||||||
.equals(theme.getSpec().getSettingName()))
|
return Mono.error(new IllegalStateException(
|
||||||
.count() > 1) {
|
"Theme must only have one settings.yaml or settings.yml."));
|
||||||
throw new IllegalStateException(
|
}
|
||||||
"Theme must only have one settings.yaml or settings.yml.");
|
if (unstructureds.stream()
|
||||||
}
|
.filter(hasConfigYaml(theme))
|
||||||
if (unstructureds.stream()
|
.count() > 1) {
|
||||||
.filter(unstructured -> unstructured.getKind().equals(ConfigMap.KIND))
|
return Mono.error(new IllegalStateException(
|
||||||
.filter(unstructured -> unstructured.getMetadata().getName()
|
"Theme must only have one config.yaml or config.yml."));
|
||||||
.equals(theme.getSpec().getConfigMapName()))
|
}
|
||||||
.count() > 1) {
|
return Flux.fromIterable(unstructureds)
|
||||||
throw new IllegalStateException(
|
.flatMap(unstructured -> {
|
||||||
"Theme must only have one config.yaml or config.yml.");
|
var spec = theme.getSpec();
|
||||||
}
|
String name = unstructured.getMetadata().getName();
|
||||||
Theme.ThemeSpec spec = theme.getSpec();
|
|
||||||
for (Unstructured unstructured : unstructureds) {
|
|
||||||
String name = unstructured.getMetadata().getName();
|
|
||||||
|
|
||||||
boolean isThemeSetting = unstructured.getKind().equals(Setting.KIND)
|
boolean isThemeSetting = unstructured.getKind().equals(Setting.KIND)
|
||||||
&& StringUtils.equals(spec.getSettingName(), name);
|
&& StringUtils.equals(spec.getSettingName(), name);
|
||||||
|
|
||||||
boolean isThemeConfig = unstructured.getKind().equals(ConfigMap.KIND)
|
boolean isThemeConfig = unstructured.getKind().equals(ConfigMap.KIND)
|
||||||
&& StringUtils.equals(spec.getConfigMapName(), name);
|
&& StringUtils.equals(spec.getConfigMapName(), name);
|
||||||
if (isThemeSetting || isThemeConfig) {
|
if (isThemeSetting || isThemeConfig) {
|
||||||
client.create(unstructured);
|
return client.create(unstructured);
|
||||||
}
|
}
|
||||||
}
|
return Mono.empty();
|
||||||
return theme;
|
})
|
||||||
|
.then(Mono.just(theme));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path getThemePath(Theme theme) {
|
private Path getThemePath(Theme theme) {
|
||||||
return getThemeWorkDir().resolve(theme.getMetadata().getName());
|
return getThemeWorkDir().resolve(theme.getMetadata().getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Predicate<Unstructured> hasSettingsYaml(Theme theme) {
|
||||||
|
return unstructured -> Setting.KIND.equals(unstructured.getKind())
|
||||||
|
&& theme.getSpec().getSettingName().equals(unstructured.getMetadata().getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Predicate<Unstructured> hasConfigYaml(Theme theme) {
|
||||||
|
return unstructured -> ConfigMap.KIND.equals(unstructured.getKind())
|
||||||
|
&& theme.getSpec().getConfigMapName().equals(unstructured.getMetadata().getName());
|
||||||
|
}
|
||||||
|
|
||||||
static class ThemeUtils {
|
static class ThemeUtils {
|
||||||
private static final String THEME_TMP_PREFIX = "halo-theme-";
|
private static final String THEME_TMP_PREFIX = "halo-theme-";
|
||||||
private static final String[] themeManifests = {"theme.yaml", "theme.yml"};
|
private static final String[] themeManifests = {"theme.yaml", "theme.yml"};
|
||||||
|
|
|
@ -15,7 +15,6 @@ import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||||
import org.springframework.http.HttpStatus;
|
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
|
||||||
|
@ -24,25 +23,24 @@ import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
|
||||||
import org.springframework.web.server.ServerWebInputException;
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.core.extension.service.UserService;
|
import run.halo.app.core.extension.service.UserService;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class UserEndpoint implements CustomEndpoint {
|
public class UserEndpoint implements CustomEndpoint {
|
||||||
|
|
||||||
private static final String SELF_USER = "-";
|
private static final String SELF_USER = "-";
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
|
|
||||||
public UserEndpoint(ExtensionClient client, UserService userService) {
|
public UserEndpoint(ReactiveExtensionClient client, UserService userService) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.userService = userService;
|
this.userService = userService;
|
||||||
}
|
}
|
||||||
|
@ -118,10 +116,9 @@ public class UserEndpoint implements CustomEndpoint {
|
||||||
@NonNull
|
@NonNull
|
||||||
Mono<ServerResponse> me(ServerRequest request) {
|
Mono<ServerResponse> me(ServerRequest request) {
|
||||||
return ReactiveSecurityContextHolder.getContext()
|
return ReactiveSecurityContextHolder.getContext()
|
||||||
.map(ctx -> {
|
.flatMap(ctx -> {
|
||||||
var name = ctx.getAuthentication().getName();
|
var name = ctx.getAuthentication().getName();
|
||||||
return client.fetch(User.class, name)
|
return client.get(User.class, name);
|
||||||
.orElseThrow(() -> new ExtensionNotFoundException(name));
|
|
||||||
})
|
})
|
||||||
.flatMap(user -> ServerResponse.ok()
|
.flatMap(user -> ServerResponse.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
@ -134,18 +131,15 @@ public class UserEndpoint implements CustomEndpoint {
|
||||||
return request.bodyToMono(GrantRequest.class)
|
return request.bodyToMono(GrantRequest.class)
|
||||||
.switchIfEmpty(
|
.switchIfEmpty(
|
||||||
Mono.error(() -> new ServerWebInputException("Request body is empty")))
|
Mono.error(() -> new ServerWebInputException("Request body is empty")))
|
||||||
.flatMap(grant -> {
|
.flatMap(grant -> client.get(User.class, username).thenReturn(grant))
|
||||||
// preflight check
|
.flatMap(grant -> Flux.fromIterable(grant.roles)
|
||||||
client.fetch(User.class, username)
|
.flatMap(roleName -> client.get(Role.class, roleName))
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
|
.then().thenReturn(grant))
|
||||||
"User " + username + " was not found"));
|
.zipWith(client.list(RoleBinding.class, RoleBinding.containsUser(username), null)
|
||||||
|
.collectList())
|
||||||
grant.roles.forEach(roleName -> client.fetch(Role.class, roleName)
|
.flatMap(tuple2 -> {
|
||||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
|
var grant = tuple2.getT1();
|
||||||
"Role " + roleName + " was not found")));
|
var bindings = tuple2.getT2();
|
||||||
|
|
||||||
var bindings =
|
|
||||||
client.list(RoleBinding.class, RoleBinding.containsUser(username), null);
|
|
||||||
var bindingToUpdate = new HashSet<RoleBinding>();
|
var bindingToUpdate = new HashSet<RoleBinding>();
|
||||||
var bindingToDelete = new HashSet<RoleBinding>();
|
var bindingToDelete = new HashSet<RoleBinding>();
|
||||||
var existingRoles = new HashSet<String>();
|
var existingRoles = new HashSet<String>();
|
||||||
|
|
|
@ -14,11 +14,12 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
||||||
import run.halo.app.core.extension.RoleBinding.Subject;
|
import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -29,23 +30,28 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
@Service
|
@Service
|
||||||
public class DefaultRoleService implements RoleService {
|
public class DefaultRoleService implements RoleService {
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
private final ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
public DefaultRoleService(ExtensionClient extensionClient) {
|
public DefaultRoleService(ReactiveExtensionClient extensionClient) {
|
||||||
this.extensionClient = extensionClient;
|
this.extensionClient = extensionClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@NonNull
|
@NonNull
|
||||||
public Role getRole(@NonNull String name) {
|
public Role getRole(@NonNull String name) {
|
||||||
return extensionClient.fetch(Role.class, name).orElseThrow();
|
return extensionClient.fetch(Role.class, name).blockOptional().orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Role> getMonoRole(String name) {
|
||||||
|
return extensionClient.get(Role.class, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<RoleRef> listRoleRefs(Subject subject) {
|
public Flux<RoleRef> listRoleRefs(Subject subject) {
|
||||||
return Flux.fromIterable(extensionClient.list(RoleBinding.class,
|
return extensionClient.list(RoleBinding.class,
|
||||||
binding -> binding.getSubjects().contains(subject),
|
binding -> binding.getSubjects().contains(subject),
|
||||||
null))
|
null)
|
||||||
.map(RoleBinding::getRoleRef);
|
.map(RoleBinding::getRoleRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,16 +73,16 @@ public class DefaultRoleService implements RoleService {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
visited.add(roleName);
|
visited.add(roleName);
|
||||||
extensionClient.fetch(Role.class, roleName).ifPresent(role -> {
|
extensionClient.fetch(Role.class, roleName)
|
||||||
result.add(role);
|
.subscribe(role -> {
|
||||||
// add role dependencies to queue
|
result.add(role);
|
||||||
Map<String, String> annotations = role.getMetadata().getAnnotations();
|
Map<String, String> annotations = role.getMetadata().getAnnotations();
|
||||||
if (annotations != null) {
|
if (annotations != null) {
|
||||||
String roleNameDependencies = annotations.get(Role.ROLE_DEPENDENCIES_ANNO);
|
String roleNameDependencies = annotations.get(Role.ROLE_DEPENDENCIES_ANNO);
|
||||||
List<String> roleDependencies = stringToList(roleNameDependencies);
|
List<String> roleDependencies = stringToList(roleNameDependencies);
|
||||||
queue.addAll(roleDependencies);
|
queue.addAll(roleDependencies);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
import run.halo.app.core.extension.RoleBinding.RoleRef;
|
||||||
import run.halo.app.core.extension.RoleBinding.Subject;
|
import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
|
@ -15,8 +16,11 @@ import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
public interface RoleService {
|
public interface RoleService {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@Deprecated
|
||||||
Role getRole(String name);
|
Role getRole(String name);
|
||||||
|
|
||||||
|
Mono<Role> getMonoRole(String name);
|
||||||
|
|
||||||
Flux<RoleRef> listRoleRefs(Subject subject);
|
Flux<RoleRef> listRoleRefs(Subject subject);
|
||||||
|
|
||||||
List<Role> listDependencies(Set<String> names);
|
List<Role> listDependencies(Set<String> names);
|
||||||
|
|
|
@ -9,8 +9,7 @@ public interface UserService {
|
||||||
|
|
||||||
Mono<User> getUser(String username);
|
Mono<User> getUser(String username);
|
||||||
|
|
||||||
@Deprecated
|
Mono<User> updatePassword(String username, String newPassword);
|
||||||
Mono<Void> updatePassword(String username, String newPassword);
|
|
||||||
|
|
||||||
Mono<User> updateWithRawPassword(String username, String rawPassword);
|
Mono<User> updateWithRawPassword(String username, String rawPassword);
|
||||||
|
|
||||||
|
|
|
@ -3,72 +3,58 @@ package run.halo.app.core.extension.service;
|
||||||
import static run.halo.app.core.extension.RoleBinding.containsUser;
|
import static run.halo.app.core.extension.RoleBinding.containsUser;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.function.Predicate;
|
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class UserServiceImpl implements UserService {
|
public class UserServiceImpl implements UserService {
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public UserServiceImpl(ExtensionClient client, PasswordEncoder passwordEncoder) {
|
public UserServiceImpl(ReactiveExtensionClient client, PasswordEncoder passwordEncoder) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<User> getUser(String username) {
|
public Mono<User> getUser(String username) {
|
||||||
return Mono.justOrEmpty(client.fetch(User.class, username));
|
return client.get(User.class, username);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> updatePassword(String username, String newPassword) {
|
public Mono<User> updatePassword(String username, String newPassword) {
|
||||||
return getUser(username)
|
return getUser(username)
|
||||||
.doOnNext(user -> {
|
.filter(user -> !Objects.equals(user.getSpec().getPassword(), newPassword))
|
||||||
|
.flatMap(user -> {
|
||||||
user.getSpec().setPassword(newPassword);
|
user.getSpec().setPassword(newPassword);
|
||||||
client.update(user);
|
return client.update(user);
|
||||||
})
|
});
|
||||||
.then();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<User> updateWithRawPassword(String username, String rawPassword) {
|
public Mono<User> updateWithRawPassword(String username, String rawPassword) {
|
||||||
return getUser(username)
|
return getUser(username)
|
||||||
.filter(Predicate.not(hasPassword().and(passwordMatches(rawPassword))))
|
.filter(user -> !passwordEncoder.matches(rawPassword, user.getSpec().getPassword()))
|
||||||
.flatMap(user -> {
|
.flatMap(user -> {
|
||||||
// TODO Validate the password
|
|
||||||
user.getSpec().setPassword(passwordEncoder.encode(rawPassword));
|
user.getSpec().setPassword(passwordEncoder.encode(rawPassword));
|
||||||
client.update(user);
|
return client.update(user);
|
||||||
// get the latest user
|
|
||||||
return getUser(username);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Predicate<User> hasPassword() {
|
|
||||||
return user -> StringUtils.hasText(user.getSpec().getPassword());
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate<User> passwordMatches(String rawPassword) {
|
|
||||||
return user -> passwordEncoder.matches(rawPassword, user.getSpec().getPassword());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Flux<Role> listRoles(String name) {
|
public Flux<Role> listRoles(String name) {
|
||||||
return Flux.fromStream(client.list(RoleBinding.class, containsUser(name), null)
|
return client.list(RoleBinding.class, containsUser(name), null)
|
||||||
.stream()
|
|
||||||
.filter(roleBinding -> Role.KIND.equals(roleBinding.getRoleRef().getKind()))
|
.filter(roleBinding -> Role.KIND.equals(roleBinding.getRoleRef().getKind()))
|
||||||
.map(roleBinding -> roleBinding.getRoleRef().getName())
|
.map(roleBinding -> roleBinding.getRoleRef().getName())
|
||||||
.map(roleName -> client.fetch(Role.class, roleName).orElse(null))
|
.flatMap(roleName -> client.fetch(Role.class, roleName));
|
||||||
.filter(Objects::nonNull));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.extension.store.ExtensionStoreClient;
|
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
|
|
||||||
|
@ -13,6 +14,7 @@ import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
|
@Component
|
||||||
public class DefaultExtensionClient implements ExtensionClient {
|
public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
private final ExtensionStoreClient storeClient;
|
private final ExtensionStoreClient storeClient;
|
||||||
|
@ -22,12 +24,15 @@ public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
private final Watcher.WatcherComposite watchers;
|
private final Watcher.WatcherComposite watchers;
|
||||||
|
|
||||||
|
private final ReactiveExtensionClient reactiveClient;
|
||||||
|
|
||||||
public DefaultExtensionClient(ExtensionStoreClient storeClient,
|
public DefaultExtensionClient(ExtensionStoreClient storeClient,
|
||||||
ExtensionConverter converter,
|
ExtensionConverter converter,
|
||||||
SchemeManager schemeManager) {
|
SchemeManager schemeManager, ReactiveExtensionClient reactiveClient) {
|
||||||
this.storeClient = storeClient;
|
this.storeClient = storeClient;
|
||||||
this.converter = converter;
|
this.converter = converter;
|
||||||
this.schemeManager = schemeManager;
|
this.schemeManager = schemeManager;
|
||||||
|
this.reactiveClient = reactiveClient;
|
||||||
|
|
||||||
watchers = new Watcher.WatcherComposite();
|
watchers = new Watcher.WatcherComposite();
|
||||||
}
|
}
|
||||||
|
@ -116,6 +121,8 @@ public class DefaultExtensionClient implements ExtensionClient {
|
||||||
@Override
|
@Override
|
||||||
public void watch(Watcher watcher) {
|
public void watch(Watcher watcher) {
|
||||||
this.watchers.addWatcher(watcher);
|
this.watchers.addWatcher(watcher);
|
||||||
|
// TODO Refactor the watch process. At present, we have to ensure the compatibility.
|
||||||
|
reactiveClient.watch(watcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
package run.halo.app.extension;
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extension is an interface which represents an Extension. It contains setters and getters of
|
* Extension is an interface which represents an Extension. It contains setters and getters of
|
||||||
* GroupVersionKind and Metadata.
|
* GroupVersionKind and Metadata.
|
||||||
*/
|
*/
|
||||||
public interface Extension extends ExtensionOperator {
|
public interface Extension extends ExtensionOperator, Comparable<Extension> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default int compareTo(Extension another) {
|
||||||
|
if (another == null || another.getMetadata() == null) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (getMetadata() == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return Objects.compare(getMetadata().getName(), another.getMetadata().getName(),
|
||||||
|
Comparator.naturalOrder());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,9 @@ import java.util.function.Predicate;
|
||||||
* ExtensionStore.
|
* ExtensionStore.
|
||||||
*
|
*
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
|
* @deprecated Use {@link ReactiveExtensionClient} instead.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "2.0")
|
||||||
public interface ExtensionClient {
|
public interface ExtensionClient {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||||
import org.openapi4j.core.exception.ResolutionException;
|
import org.openapi4j.core.exception.ResolutionException;
|
||||||
import org.openapi4j.schema.validator.ValidationData;
|
import org.openapi4j.schema.validator.ValidationData;
|
||||||
import org.openapi4j.schema.validator.v3.SchemaValidator;
|
import org.openapi4j.schema.validator.v3.SchemaValidator;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.SchemaViolationException;
|
import run.halo.app.extension.exception.SchemaViolationException;
|
||||||
import run.halo.app.extension.store.ExtensionStore;
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
@ -17,6 +18,7 @@ import run.halo.app.extension.store.ExtensionStore;
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Component
|
||||||
public class JSONExtensionConverter implements ExtensionConverter {
|
public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
|
|
||||||
public final ObjectMapper objectMapper;
|
public final ObjectMapper objectMapper;
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionClient is an interface which contains some operations on Extension instead of
|
||||||
|
* ExtensionStore.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public interface ReactiveExtensionClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists Extensions by Extension type, filter and sorter.
|
||||||
|
*
|
||||||
|
* @param type is the class type of Extension.
|
||||||
|
* @param predicate filters the reEnqueue.
|
||||||
|
* @param comparator sorts the reEnqueue.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return all filtered and sorted Extensions.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparator);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists Extensions by Extension type, filter, sorter and page info.
|
||||||
|
*
|
||||||
|
* @param type is the class type of Extension.
|
||||||
|
* @param predicate filters the reEnqueue.
|
||||||
|
* @param comparator sorts the reEnqueue.
|
||||||
|
* @param page is page number which starts from 0.
|
||||||
|
* @param size is page size.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return a list of Extensions.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparator, int page, int size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches Extension by its type and name.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @param name is Extension name.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return an optional Extension.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Mono<E> fetch(Class<E> type, String name);
|
||||||
|
|
||||||
|
Mono<Unstructured> fetch(GroupVersionKind gvk, String name);
|
||||||
|
|
||||||
|
<E extends Extension> Mono<E> get(Class<E> type, String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Extension.
|
||||||
|
*
|
||||||
|
* @param extension is fresh Extension to be created. Please make sure the Extension name does
|
||||||
|
* not exist.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Mono<E> create(E extension);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an Extension.
|
||||||
|
*
|
||||||
|
* @param extension is an Extension to be updated. Please make sure the resource version is
|
||||||
|
* latest.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Mono<E> update(E extension);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an Extension.
|
||||||
|
*
|
||||||
|
* @param extension is an Extension to be deleted. Please make sure the resource version is
|
||||||
|
* latest.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Mono<E> delete(E extension);
|
||||||
|
|
||||||
|
void watch(Watcher watcher);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import org.springframework.data.util.Predicates;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ReactiveExtensionClientImpl implements ReactiveExtensionClient {
|
||||||
|
|
||||||
|
private final ReactiveExtensionStoreClient client;
|
||||||
|
|
||||||
|
private final ExtensionConverter converter;
|
||||||
|
|
||||||
|
private final SchemeManager schemeManager;
|
||||||
|
|
||||||
|
private final Watcher.WatcherComposite watchers;
|
||||||
|
|
||||||
|
public ReactiveExtensionClientImpl(ReactiveExtensionStoreClient client,
|
||||||
|
ExtensionConverter converter, SchemeManager schemeManager) {
|
||||||
|
this.client = client;
|
||||||
|
this.converter = converter;
|
||||||
|
this.schemeManager = schemeManager;
|
||||||
|
this.watchers = new Watcher.WatcherComposite();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Flux<E> list(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparator) {
|
||||||
|
var scheme = schemeManager.get(type);
|
||||||
|
var prefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
||||||
|
|
||||||
|
return client.listByNamePrefix(prefix)
|
||||||
|
.map(extensionStore -> converter.convertFrom(type, extensionStore))
|
||||||
|
.filter(predicate == null ? Predicates.isTrue() : predicate)
|
||||||
|
.sort(comparator == null ? Comparator.naturalOrder() : comparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<ListResult<E>> list(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparator, int page, int size) {
|
||||||
|
var extensions = list(type, predicate, comparator);
|
||||||
|
var totalMono = extensions.count();
|
||||||
|
if (page > 0) {
|
||||||
|
extensions = extensions.skip(((long) (page - 1)) * (long) size);
|
||||||
|
}
|
||||||
|
if (size > 0) {
|
||||||
|
extensions = extensions.take(size);
|
||||||
|
}
|
||||||
|
return extensions.collectList().zipWith(totalMono)
|
||||||
|
.map(tuple -> {
|
||||||
|
List<E> content = tuple.getT1();
|
||||||
|
Long total = tuple.getT2();
|
||||||
|
return new ListResult<>(page, size, total, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<E> fetch(Class<E> type, String name) {
|
||||||
|
var storeName = ExtensionUtil.buildStoreName(schemeManager.get(type), name);
|
||||||
|
return client.fetchByName(storeName)
|
||||||
|
.map(extensionStore -> converter.convertFrom(type, extensionStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Unstructured> fetch(GroupVersionKind gvk, String name) {
|
||||||
|
var storeName = ExtensionUtil.buildStoreName(schemeManager.get(gvk), name);
|
||||||
|
return client.fetchByName(storeName)
|
||||||
|
.map(extensionStore -> converter.convertFrom(Unstructured.class, extensionStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<E> get(Class<E> type, String name) {
|
||||||
|
return fetch(type, name)
|
||||||
|
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
|
||||||
|
"Extension " + type.getName() + " with name " + name + " not found")));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<Unstructured> get(GroupVersionKind gvk, String name) {
|
||||||
|
return fetch(gvk, name)
|
||||||
|
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
|
||||||
|
"Extension " + gvk + " with name " + name + " not found")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<E> create(E extension) {
|
||||||
|
var metadata = extension.getMetadata();
|
||||||
|
// those fields should be managed by halo.
|
||||||
|
metadata.setCreationTimestamp(Instant.now());
|
||||||
|
metadata.setDeletionTimestamp(null);
|
||||||
|
metadata.setVersion(null);
|
||||||
|
|
||||||
|
var extensionStore = converter.convertTo(extension);
|
||||||
|
|
||||||
|
return client.create(extensionStore.getName(), extensionStore.getData())
|
||||||
|
.map(created -> converter.convertFrom((Class<E>) extension.getClass(), created))
|
||||||
|
.doOnNext(watchers::onAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<E> update(E extension) {
|
||||||
|
// overwrite some fields
|
||||||
|
Mono<? extends Extension> mono;
|
||||||
|
if (extension instanceof Unstructured unstructured) {
|
||||||
|
mono = get(unstructured.groupVersionKind(), extension.getMetadata().getName());
|
||||||
|
} else {
|
||||||
|
mono = get(extension.getClass(), extension.getMetadata().getName());
|
||||||
|
}
|
||||||
|
return mono
|
||||||
|
.map(old -> {
|
||||||
|
// reset some fields
|
||||||
|
var oldMetadata = old.getMetadata();
|
||||||
|
var newMetadata = extension.getMetadata();
|
||||||
|
newMetadata.setCreationTimestamp(oldMetadata.getCreationTimestamp());
|
||||||
|
newMetadata.setDeletionTimestamp(oldMetadata.getDeletionTimestamp());
|
||||||
|
extension.setMetadata(newMetadata);
|
||||||
|
return converter.convertTo(extension);
|
||||||
|
})
|
||||||
|
.flatMap(extensionStore -> client.update(extensionStore.getName(),
|
||||||
|
extensionStore.getVersion(),
|
||||||
|
extensionStore.getData()))
|
||||||
|
.map(updated -> converter.convertFrom((Class<E>) extension.getClass(), updated))
|
||||||
|
.doOnNext(updated -> watchers.onUpdate(extension, updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Mono<E> delete(E extension) {
|
||||||
|
// set deletionTimestamp
|
||||||
|
extension.getMetadata().setDeletionTimestamp(Instant.now());
|
||||||
|
var extensionStore = converter.convertTo(extension);
|
||||||
|
return client.update(extensionStore.getName(), extensionStore.getVersion(),
|
||||||
|
extensionStore.getData())
|
||||||
|
.map(deleted -> converter.convertFrom((Class<E>) extension.getClass(), deleted))
|
||||||
|
.doOnNext(watchers::onDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void watch(Watcher watcher) {
|
||||||
|
this.watchers.addWatcher(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.SchemeWatcherManager;
|
import run.halo.app.extension.SchemeWatcherManager;
|
||||||
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
import run.halo.app.extension.SchemeWatcherManager.SchemeWatcher;
|
||||||
|
@ -21,9 +21,9 @@ public class ExtensionCompositeRouterFunction implements
|
||||||
|
|
||||||
private final Map<Scheme, RouterFunction<ServerResponse>> schemeRouterFuncMapper;
|
private final Map<Scheme, RouterFunction<ServerResponse>> schemeRouterFuncMapper;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public ExtensionCompositeRouterFunction(ExtensionClient client,
|
public ExtensionCompositeRouterFunction(ReactiveExtensionClient client,
|
||||||
SchemeWatcherManager watcherManager) {
|
SchemeWatcherManager watcherManager) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
schemeRouterFuncMapper = new ConcurrentHashMap<>();
|
schemeRouterFuncMapper = new ConcurrentHashMap<>();
|
||||||
|
|
|
@ -6,19 +6,18 @@ import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|
||||||
|
|
||||||
class ExtensionCreateHandler implements ExtensionRouterFunctionFactory.CreateHandler {
|
class ExtensionCreateHandler implements ExtensionRouterFunctionFactory.CreateHandler {
|
||||||
|
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public ExtensionCreateHandler(Scheme scheme, ExtensionClient client) {
|
public ExtensionCreateHandler(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -29,18 +28,11 @@ class ExtensionCreateHandler implements ExtensionRouterFunctionFactory.CreateHan
|
||||||
return request.bodyToMono(Unstructured.class)
|
return request.bodyToMono(Unstructured.class)
|
||||||
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
|
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
|
||||||
"Cannot read body to " + scheme.groupVersionKind())))
|
"Cannot read body to " + scheme.groupVersionKind())))
|
||||||
.flatMap(extToCreate -> Mono.fromCallable(() -> {
|
.flatMap(client::create)
|
||||||
var name = extToCreate.getMetadata().getName();
|
|
||||||
client.create(extToCreate);
|
|
||||||
return client.fetch(scheme.type(), name)
|
|
||||||
.orElseThrow(() -> new ExtensionNotFoundException(
|
|
||||||
"Extension with name " + name + " was not found"));
|
|
||||||
}))
|
|
||||||
.flatMap(createdExt -> ServerResponse
|
.flatMap(createdExt -> ServerResponse
|
||||||
.created(URI.create(pathPattern() + "/" + createdExt.getMetadata().getName()))
|
.created(URI.create(pathPattern() + "/" + createdExt.getMetadata().getName()))
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(createdExt))
|
.bodyValue(createdExt));
|
||||||
.cast(ServerResponse.class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -4,39 +4,29 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.Extension;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|
||||||
|
|
||||||
class ExtensionDeleteHandler implements ExtensionRouterFunctionFactory.DeleteHandler {
|
class ExtensionDeleteHandler implements ExtensionRouterFunctionFactory.DeleteHandler {
|
||||||
|
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
ExtensionDeleteHandler(Scheme scheme, ExtensionClient client) {
|
ExtensionDeleteHandler(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<ServerResponse> handle(ServerRequest request) {
|
public Mono<ServerResponse> handle(ServerRequest request) {
|
||||||
String name = request.pathVariable("name");
|
var name = request.pathVariable("name");
|
||||||
return getExtension(name)
|
return client.get(scheme.type(), name)
|
||||||
.flatMap(extension ->
|
.flatMap(client::delete)
|
||||||
Mono.fromRunnable(() -> client.delete(extension)).thenReturn(extension))
|
.flatMap(deleted -> ServerResponse
|
||||||
.flatMap(extension -> this.getExtension(name))
|
|
||||||
.flatMap(extension -> ServerResponse
|
|
||||||
.ok()
|
.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(extension));
|
.bodyValue(deleted));
|
||||||
}
|
|
||||||
|
|
||||||
private Mono<? extends Extension> getExtension(String name) {
|
|
||||||
return Mono.justOrEmpty(client.fetch(scheme.type(), name))
|
|
||||||
.switchIfEmpty(Mono.error(() -> new ExtensionNotFoundException(
|
|
||||||
"Extension with name " + name + " was not found")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -5,16 +5,15 @@ import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|
||||||
|
|
||||||
class ExtensionGetHandler implements ExtensionRouterFunctionFactory.GetHandler {
|
class ExtensionGetHandler implements ExtensionRouterFunctionFactory.GetHandler {
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public ExtensionGetHandler(Scheme scheme, ExtensionClient client) {
|
public ExtensionGetHandler(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -30,12 +29,9 @@ class ExtensionGetHandler implements ExtensionRouterFunctionFactory.GetHandler {
|
||||||
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
|
public Mono<ServerResponse> handle(@NonNull ServerRequest request) {
|
||||||
var extensionName = request.pathVariable("name");
|
var extensionName = request.pathVariable("name");
|
||||||
|
|
||||||
var extension = client.fetch(scheme.type(), extensionName)
|
return client.get(scheme.type(), extensionName)
|
||||||
.orElseThrow(() -> new ExtensionNotFoundException(
|
.flatMap(extension -> ServerResponse.ok()
|
||||||
scheme.groupVersionKind() + " was not found"));
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
return ServerResponse
|
.bodyValue(extension));
|
||||||
.ok()
|
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
|
||||||
.bodyValue(extension);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,15 +8,15 @@ import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
|
|
||||||
class ExtensionListHandler implements ExtensionRouterFunctionFactory.ListHandler {
|
class ExtensionListHandler implements ExtensionRouterFunctionFactory.ListHandler {
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public ExtensionListHandler(Scheme scheme, ExtensionClient client) {
|
public ExtensionListHandler(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -38,12 +38,12 @@ class ExtensionListHandler implements ExtensionRouterFunctionFactory.ListHandler
|
||||||
var fieldSelectors = request.queryParams().get("fieldSelector");
|
var fieldSelectors = request.queryParams().get("fieldSelector");
|
||||||
|
|
||||||
// TODO Resolve comparator from request
|
// TODO Resolve comparator from request
|
||||||
var listResult = client.list(scheme.type(),
|
return client.list(scheme.type(),
|
||||||
labelAndFieldSelectorToPredicate(labelSelectors, fieldSelectors), null, page, size);
|
labelAndFieldSelectorToPredicate(labelSelectors, fieldSelectors), null, page, size)
|
||||||
return ServerResponse
|
.flatMap(listResult -> ServerResponse
|
||||||
.ok()
|
.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(listResult);
|
.bodyValue(listResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -12,17 +12,17 @@ import org.springframework.lang.NonNull;
|
||||||
import org.springframework.web.reactive.function.server.HandlerFunction;
|
import org.springframework.web.reactive.function.server.HandlerFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
|
|
||||||
public class ExtensionRouterFunctionFactory {
|
public class ExtensionRouterFunctionFactory {
|
||||||
|
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public ExtensionRouterFunctionFactory(Scheme scheme, ExtensionClient client) {
|
public ExtensionRouterFunctionFactory(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,20 +5,19 @@ import org.springframework.http.MediaType;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
|
||||||
|
|
||||||
class ExtensionUpdateHandler implements ExtensionRouterFunctionFactory.UpdateHandler {
|
class ExtensionUpdateHandler implements ExtensionRouterFunctionFactory.UpdateHandler {
|
||||||
|
|
||||||
private final Scheme scheme;
|
private final Scheme scheme;
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
ExtensionUpdateHandler(Scheme scheme, ExtensionClient client) {
|
ExtensionUpdateHandler(Scheme scheme, ReactiveExtensionClient client) {
|
||||||
this.scheme = scheme;
|
this.scheme = scheme;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
@ -30,14 +29,9 @@ class ExtensionUpdateHandler implements ExtensionRouterFunctionFactory.UpdateHan
|
||||||
.filter(unstructured -> unstructured.getMetadata() != null
|
.filter(unstructured -> unstructured.getMetadata() != null
|
||||||
&& StringUtils.hasText(unstructured.getMetadata().getName())
|
&& StringUtils.hasText(unstructured.getMetadata().getName())
|
||||||
&& Objects.equals(unstructured.getMetadata().getName(), name))
|
&& Objects.equals(unstructured.getMetadata().getName(), name))
|
||||||
.switchIfEmpty(Mono.error(() -> new ExtensionConvertException(
|
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
||||||
"Cannot read body to " + scheme.groupVersionKind())))
|
"Cannot read body to " + scheme.groupVersionKind())))
|
||||||
.flatMap(extToUpdate -> Mono.fromCallable(() -> {
|
.flatMap(client::update)
|
||||||
client.update(extToUpdate);
|
|
||||||
return client.fetch(scheme.type(), name)
|
|
||||||
.orElseThrow(() -> new ExtensionNotFoundException(
|
|
||||||
"Extension with name " + name + " was not found"));
|
|
||||||
}))
|
|
||||||
.flatMap(updated -> ServerResponse
|
.flatMap(updated -> ServerResponse
|
||||||
.ok()
|
.ok()
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package run.halo.app.extension.store;
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
|
||||||
import jakarta.persistence.Id;
|
|
||||||
import jakarta.persistence.Lob;
|
import jakarta.persistence.Lob;
|
||||||
import jakarta.persistence.Version;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.annotation.Version;
|
||||||
|
import org.springframework.data.relational.core.mapping.Table;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ExtensionStore is an entity for storing Extension data into database.
|
* ExtensionStore is an entity for storing Extension data into database.
|
||||||
|
@ -12,7 +12,7 @@ import lombok.Data;
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
@Data
|
@Data
|
||||||
@Entity(name = "extensions")
|
@Table(name = "extensions")
|
||||||
public class ExtensionStore {
|
public class ExtensionStore {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -22,31 +22,31 @@ public class ExtensionStoreClientJPAImpl implements ExtensionStoreClient {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ExtensionStore> listByNamePrefix(String prefix) {
|
public List<ExtensionStore> listByNamePrefix(String prefix) {
|
||||||
return repository.findAllByNameStartingWith(prefix);
|
return repository.findAllByNameStartingWith(prefix).collectList().block();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<ExtensionStore> fetchByName(String name) {
|
public Optional<ExtensionStore> fetchByName(String name) {
|
||||||
return repository.findById(name);
|
return repository.findById(name).blockOptional();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExtensionStore create(String name, byte[] data) {
|
public ExtensionStore create(String name, byte[] data) {
|
||||||
var store = new ExtensionStore(name, data);
|
var store = new ExtensionStore(name, data);
|
||||||
return repository.save(store);
|
return repository.save(store).block();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ExtensionStore update(String name, Long version, byte[] data) {
|
public ExtensionStore update(String name, Long version, byte[] data) {
|
||||||
var store = new ExtensionStore(name, data, version);
|
var store = new ExtensionStore(name, data, version);
|
||||||
return repository.save(store);
|
return repository.save(store).block();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public ExtensionStore delete(String name, Long version) {
|
public ExtensionStore delete(String name, Long version) {
|
||||||
var extensionStore =
|
var extensionStore =
|
||||||
repository.findById(name).orElseThrow(EntityNotFoundException::new);
|
repository.findById(name).blockOptional().orElseThrow(EntityNotFoundException::new);
|
||||||
extensionStore.setVersion(version);
|
extensionStore.setVersion(version);
|
||||||
repository.delete(extensionStore);
|
repository.delete(extensionStore);
|
||||||
return extensionStore;
|
return extensionStore;
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package run.halo.app.extension.store;
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
import java.util.List;
|
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This repository contains some basic operations on ExtensionStore entity.
|
* This repository contains some basic operations on ExtensionStore entity.
|
||||||
|
@ -10,7 +10,7 @@ import org.springframework.stereotype.Repository;
|
||||||
* @author johnniang
|
* @author johnniang
|
||||||
*/
|
*/
|
||||||
@Repository
|
@Repository
|
||||||
public interface ExtensionStoreRepository extends JpaRepository<ExtensionStore, String> {
|
public interface ExtensionStoreRepository extends R2dbcRepository<ExtensionStore, String> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all ExtensionStore by name prefix.
|
* Finds all ExtensionStore by name prefix.
|
||||||
|
@ -18,6 +18,6 @@ public interface ExtensionStoreRepository extends JpaRepository<ExtensionStore,
|
||||||
* @param prefix is the prefix of name.
|
* @param prefix is the prefix of name.
|
||||||
* @return all ExtensionStores which names starts with the given prefix.
|
* @return all ExtensionStores which names starts with the given prefix.
|
||||||
*/
|
*/
|
||||||
List<ExtensionStore> findAllByNameStartingWith(String prefix);
|
Flux<ExtensionStore> findAllByNameStartingWith(String prefix);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
public interface ReactiveExtensionStoreClient {
|
||||||
|
|
||||||
|
Flux<ExtensionStore> listByNamePrefix(String prefix);
|
||||||
|
|
||||||
|
Mono<ExtensionStore> fetchByName(String name);
|
||||||
|
|
||||||
|
Mono<ExtensionStore> create(String name, byte[] data);
|
||||||
|
|
||||||
|
Mono<ExtensionStore> update(String name, Long version, byte[] data);
|
||||||
|
|
||||||
|
Mono<ExtensionStore> delete(String name, Long version);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ReactiveExtensionStoreClientImpl implements ReactiveExtensionStoreClient {
|
||||||
|
|
||||||
|
private final ExtensionStoreRepository repository;
|
||||||
|
|
||||||
|
public ReactiveExtensionStoreClientImpl(ExtensionStoreRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<ExtensionStore> listByNamePrefix(String prefix) {
|
||||||
|
return repository.findAllByNameStartingWith(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ExtensionStore> fetchByName(String name) {
|
||||||
|
return repository.findById(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ExtensionStore> create(String name, byte[] data) {
|
||||||
|
return repository.save(new ExtensionStore(name, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<ExtensionStore> update(String name, Long version, byte[] data) {
|
||||||
|
return repository.save(new ExtensionStore(name, data, version));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public Mono<ExtensionStore> delete(String name, Long version) {
|
||||||
|
return repository.findById(name)
|
||||||
|
.flatMap(extensionStore -> {
|
||||||
|
// reset the version
|
||||||
|
extensionStore.setVersion(version);
|
||||||
|
return repository.delete(extensionStore).thenReturn(extensionStore);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,13 +6,14 @@ import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
|
||||||
import org.springframework.lang.NonNull;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
@ -27,21 +28,21 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
public class ExtensionResourceInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
public class ExtensionResourceInitializer {
|
||||||
|
|
||||||
public static final Set<String> REQUIRED_EXTENSION_LOCATIONS =
|
public static final Set<String> REQUIRED_EXTENSION_LOCATIONS =
|
||||||
Set.of("classpath:/extensions/*.yaml", "classpath:/extensions/*.yml");
|
Set.of("classpath:/extensions/*.yaml", "classpath:/extensions/*.yml");
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
private final ExtensionClient extensionClient;
|
private final ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
public ExtensionResourceInitializer(HaloProperties haloProperties,
|
public ExtensionResourceInitializer(HaloProperties haloProperties,
|
||||||
ExtensionClient extensionClient) {
|
ReactiveExtensionClient extensionClient) {
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
this.extensionClient = extensionClient;
|
this.extensionClient = extensionClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@EventListener
|
||||||
public void onApplicationEvent(@NonNull ApplicationReadyEvent event) {
|
public Mono<Void> initialize(ApplicationReadyEvent readyEvent) {
|
||||||
var locations = new HashSet<String>();
|
var locations = new HashSet<String>();
|
||||||
if (!haloProperties.isRequiredExtensionDisabled()) {
|
if (!haloProperties.isRequiredExtensionDisabled()) {
|
||||||
locations.addAll(REQUIRED_EXTENSION_LOCATIONS);
|
locations.addAll(REQUIRED_EXTENSION_LOCATIONS);
|
||||||
|
@ -49,30 +50,35 @@ public class ExtensionResourceInitializer implements ApplicationListener<Applica
|
||||||
if (haloProperties.getInitialExtensionLocations() != null) {
|
if (haloProperties.getInitialExtensionLocations() != null) {
|
||||||
locations.addAll(haloProperties.getInitialExtensionLocations());
|
locations.addAll(haloProperties.getInitialExtensionLocations());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(locations)) {
|
if (CollectionUtils.isEmpty(locations)) {
|
||||||
return;
|
return Mono.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
var resources = locations.stream()
|
return Flux.fromIterable(locations)
|
||||||
|
.doOnNext(location ->
|
||||||
|
log.debug("Trying to initialize extension resources from location: {}", location))
|
||||||
.map(this::listResources)
|
.map(this::listResources)
|
||||||
.flatMap(List::stream)
|
|
||||||
.distinct()
|
.distinct()
|
||||||
.toArray(Resource[]::new);
|
.flatMapIterable(resources -> resources)
|
||||||
|
.doOnNext(resource -> log.debug("Initializing extension resource: {}", resource))
|
||||||
log.info("Initializing [{}] extensions in locations: {}", resources.length, locations);
|
.map(resource -> new YamlUnstructuredLoader(resource).load())
|
||||||
|
.flatMapIterable(extensions -> extensions)
|
||||||
new YamlUnstructuredLoader(resources).load()
|
.flatMap(extension -> extensionClient.fetch(extension.groupVersionKind(),
|
||||||
.forEach(unstructured -> extensionClient.fetch(unstructured.groupVersionKind(),
|
extension.getMetadata().getName())
|
||||||
unstructured.getMetadata().getName())
|
.flatMap(createdExtension -> {
|
||||||
.ifPresentOrElse(persisted -> {
|
extension.getMetadata()
|
||||||
unstructured.getMetadata()
|
.setVersion(createdExtension.getMetadata().getVersion());
|
||||||
.setVersion(persisted.getMetadata().getVersion());
|
return extensionClient.update(extension);
|
||||||
// TODO Patch the unstructured instead of update it in the future
|
})
|
||||||
extensionClient.update(unstructured);
|
.switchIfEmpty(Mono.defer(() -> extensionClient.create(extension)))
|
||||||
}, () -> extensionClient.create(unstructured)));
|
)
|
||||||
|
.doOnNext(extension -> {
|
||||||
log.info("Initialized [{}] extensions in locations: {}", resources.length, locations);
|
if (log.isDebugEnabled()) {
|
||||||
|
log.debug("Initialized extension resource: {}/{}", extension.groupVersionKind(),
|
||||||
|
extension.getMetadata().getName());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Resource> listResources(String location) {
|
private List<Resource> listResources(String location) {
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package run.halo.app.infra;
|
package run.halo.app.infra;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import org.springframework.core.convert.ConversionService;
|
import org.springframework.core.convert.ConversionService;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.ConfigMap;
|
import run.halo.app.extension.ConfigMap;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,35 +17,30 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
public class SystemConfigurableEnvironmentFetcher {
|
public class SystemConfigurableEnvironmentFetcher {
|
||||||
private static final String SYSTEM_CONFIGMAP_NAME = "system";
|
private static final String SYSTEM_CONFIGMAP_NAME = "system";
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
private final ReactiveExtensionClient extensionClient;
|
||||||
private final ConversionService conversionService;
|
private final ConversionService conversionService;
|
||||||
|
|
||||||
public SystemConfigurableEnvironmentFetcher(ExtensionClient extensionClient,
|
public SystemConfigurableEnvironmentFetcher(ReactiveExtensionClient extensionClient,
|
||||||
ConversionService conversionService) {
|
ConversionService conversionService) {
|
||||||
this.extensionClient = extensionClient;
|
this.extensionClient = extensionClient;
|
||||||
this.conversionService = conversionService;
|
this.conversionService = conversionService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> Optional<T> fetch(String key, Class<T> type) {
|
public <T> Mono<T> fetch(String key, Class<T> type) {
|
||||||
var stringValue = getInternal(key);
|
return getValuesInternal().map(map -> map.get(key))
|
||||||
if (stringValue == null) {
|
.map(stringValue -> {
|
||||||
return Optional.empty();
|
if (conversionService.canConvert(String.class, type)) {
|
||||||
}
|
return conversionService.convert(stringValue, type);
|
||||||
if (conversionService.canConvert(String.class, type)) {
|
}
|
||||||
return Optional.ofNullable(conversionService.convert(stringValue, type));
|
return JsonUtils.jsonToObject(stringValue, type);
|
||||||
}
|
});
|
||||||
return Optional.of(JsonUtils.jsonToObject(stringValue, type));
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getInternal(String group) {
|
|
||||||
return getValuesInternal().get(group);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Map<String, String> getValuesInternal() {
|
private Mono<Map<String, String>> getValuesInternal() {
|
||||||
return extensionClient.fetch(ConfigMap.class, SYSTEM_CONFIGMAP_NAME)
|
return extensionClient.fetch(ConfigMap.class, SYSTEM_CONFIGMAP_NAME)
|
||||||
.filter(configMap -> configMap.getData() != null)
|
.filter(configMap -> configMap.getData() != null)
|
||||||
.map(ConfigMap::getData)
|
.map(ConfigMap::getData)
|
||||||
.orElse(Map.of());
|
.defaultIfEmpty(Map.of());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import org.pf4j.PluginWrapper;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.context.ApplicationListener;
|
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.MultiValueMap;
|
import reactor.core.publisher.Flux;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.extension.GroupVersionKind;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -16,29 +15,29 @@ import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class PluginBeforeStopSyncListener
|
public class PluginBeforeStopSyncListener {
|
||||||
implements ApplicationListener<HaloPluginBeforeStopEvent> {
|
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public PluginBeforeStopSyncListener(ExtensionClient client) {
|
public PluginBeforeStopSyncListener(ReactiveExtensionClient client) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@EventListener
|
||||||
public void onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {
|
public Mono<Void> onApplicationEvent(@NonNull HaloPluginBeforeStopEvent event) {
|
||||||
PluginWrapper pluginWrapper = event.getPlugin();
|
var pluginWrapper = event.getPlugin();
|
||||||
PluginApplicationContext pluginContext = ExtensionContextRegistry.getInstance()
|
var pluginContext = ExtensionContextRegistry.getInstance()
|
||||||
.getByPluginId(pluginWrapper.getPluginId());
|
.getByPluginId(pluginWrapper.getPluginId());
|
||||||
|
|
||||||
cleanUpPluginExtensionResources(pluginContext);
|
return cleanUpPluginExtensionResources(pluginContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanUpPluginExtensionResources(PluginApplicationContext context) {
|
private Mono<Void> cleanUpPluginExtensionResources(PluginApplicationContext context) {
|
||||||
MultiValueMap<GroupVersionKind, String> gvkExtensionNames =
|
var gvkExtensionNames = context.extensionNamesMapping();
|
||||||
context.extensionNamesMapping();
|
return Flux.fromIterable(gvkExtensionNames.entrySet())
|
||||||
gvkExtensionNames.forEach((gvk, extensionNames) ->
|
.flatMap(entry -> Flux.fromIterable(entry.getValue())
|
||||||
extensionNames.forEach(extensionName -> client.fetch(gvk, extensionName)
|
.flatMap(extensionName -> client.fetch(entry.getKey(), extensionName))
|
||||||
.ifPresent(client::delete)));
|
.flatMap(client::delete))
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -66,30 +65,18 @@ public class PluginCompositeRouterFunction implements RouterFunction<ServerRespo
|
||||||
* @param haloPluginStartedEvent event for plugin started
|
* @param haloPluginStartedEvent event for plugin started
|
||||||
*/
|
*/
|
||||||
@EventListener(HaloPluginStartedEvent.class)
|
@EventListener(HaloPluginStartedEvent.class)
|
||||||
public void onPluginStarted(HaloPluginStartedEvent haloPluginStartedEvent) {
|
public Mono<Void> onPluginStarted(HaloPluginStartedEvent haloPluginStartedEvent) {
|
||||||
PluginWrapper plugin = haloPluginStartedEvent.getPlugin();
|
PluginWrapper plugin = haloPluginStartedEvent.getPlugin();
|
||||||
// Obtain plugin application context
|
// Obtain plugin application context
|
||||||
PluginApplicationContext pluginApplicationContext =
|
PluginApplicationContext pluginApplicationContext =
|
||||||
ExtensionContextRegistry.getInstance().getByPluginId(plugin.getPluginId());
|
ExtensionContextRegistry.getInstance().getByPluginId(plugin.getPluginId());
|
||||||
|
|
||||||
// create reverse proxy router function for plugin
|
return Flux.fromIterable(routerFunctions(pluginApplicationContext))
|
||||||
RouterFunction<ServerResponse> reverseProxyRouterFunction =
|
.concatWith(reverseProxyRouterFunctionFactory.create(pluginApplicationContext))
|
||||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
|
||||||
|
|
||||||
List<RouterFunction<ServerResponse>> routerFunctions =
|
|
||||||
routerFunctions(pluginApplicationContext);
|
|
||||||
|
|
||||||
List<RouterFunction<ServerResponse>> combinedRouterFunctions =
|
|
||||||
new ArrayList<>(routerFunctions);
|
|
||||||
if (reverseProxyRouterFunction != null) {
|
|
||||||
combinedRouterFunctions.add(reverseProxyRouterFunction);
|
|
||||||
}
|
|
||||||
|
|
||||||
combinedRouterFunctions.stream()
|
|
||||||
.reduce(RouterFunction::and)
|
.reduce(RouterFunction::and)
|
||||||
.ifPresent(compositeRouterFunction -> {
|
.doOnNext(routerFunction ->
|
||||||
routerFunctionRegistry.put(plugin.getPluginId(), compositeRouterFunction);
|
routerFunctionRegistry.put(plugin.getPluginId(), routerFunction))
|
||||||
});
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener(HaloPluginStoppedEvent.class)
|
@EventListener(HaloPluginStoppedEvent.class)
|
||||||
|
|
|
@ -7,8 +7,6 @@ import java.nio.file.Path;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.jar.JarFile;
|
import java.util.jar.JarFile;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -19,13 +17,14 @@ import org.pf4j.DevelopmentPluginClasspath;
|
||||||
import org.pf4j.PluginRuntimeException;
|
import org.pf4j.PluginRuntimeException;
|
||||||
import org.pf4j.PluginWrapper;
|
import org.pf4j.PluginWrapper;
|
||||||
import org.pf4j.RuntimeMode;
|
import org.pf4j.RuntimeMode;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.core.io.DefaultResourceLoader;
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Plugin;
|
import run.halo.app.core.extension.Plugin;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||||
|
|
||||||
|
@ -36,50 +35,55 @@ import run.halo.app.plugin.event.HaloPluginStartedEvent;
|
||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class PluginStartedListener implements ApplicationListener<HaloPluginStartedEvent> {
|
public class PluginStartedListener {
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
private final ReactiveExtensionClient client;
|
||||||
|
|
||||||
public PluginStartedListener(ExtensionClient extensionClient) {
|
public PluginStartedListener(ReactiveExtensionClient extensionClient) {
|
||||||
this.extensionClient = extensionClient;
|
this.client = extensionClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@EventListener
|
||||||
public void onApplicationEvent(HaloPluginStartedEvent event) {
|
public Mono<Void> onApplicationEvent(HaloPluginStartedEvent event) {
|
||||||
PluginWrapper pluginWrapper = event.getPlugin();
|
PluginWrapper pluginWrapper = event.getPlugin();
|
||||||
Plugin plugin =
|
var resourceLoader =
|
||||||
extensionClient.fetch(Plugin.class, pluginWrapper.getPluginId()).orElseThrow();
|
|
||||||
// load unstructured
|
|
||||||
DefaultResourceLoader resourceLoader =
|
|
||||||
new DefaultResourceLoader(pluginWrapper.getPluginClassLoader());
|
new DefaultResourceLoader(pluginWrapper.getPluginClassLoader());
|
||||||
|
var pluginApplicationContext = ExtensionContextRegistry.getInstance()
|
||||||
PluginApplicationContext pluginApplicationContext = ExtensionContextRegistry.getInstance()
|
|
||||||
.getByPluginId(pluginWrapper.getPluginId());
|
.getByPluginId(pluginWrapper.getPluginId());
|
||||||
|
return client.get(Plugin.class, pluginWrapper.getPluginId())
|
||||||
|
.zipWith(Mono.just(
|
||||||
|
lookupExtensions(pluginWrapper.getPluginPath(), pluginWrapper.getRuntimeMode())))
|
||||||
|
.flatMap(tuple2 -> {
|
||||||
|
var plugin = tuple2.getT1();
|
||||||
|
var extensionLocations = tuple2.getT2();
|
||||||
|
return Flux.fromIterable(extensionLocations)
|
||||||
|
.map(resourceLoader::getResource)
|
||||||
|
.filter(Resource::exists)
|
||||||
|
.map(resource -> new YamlUnstructuredLoader(resource).load())
|
||||||
|
.flatMapIterable(rs -> rs)
|
||||||
|
.flatMap(unstructured -> {
|
||||||
|
var metadata = unstructured.getMetadata();
|
||||||
|
// collector plugin initialize extension resources
|
||||||
|
pluginApplicationContext.addExtensionMapping(
|
||||||
|
unstructured.groupVersionKind(),
|
||||||
|
metadata.getName());
|
||||||
|
var labels = metadata.getLabels();
|
||||||
|
if (labels == null) {
|
||||||
|
labels = new HashMap<>();
|
||||||
|
}
|
||||||
|
labels.put(PluginConst.PLUGIN_NAME_LABEL_NAME,
|
||||||
|
plugin.getMetadata().getName());
|
||||||
|
metadata.setLabels(labels);
|
||||||
|
|
||||||
lookupExtensions(pluginWrapper.getPluginPath(),
|
return client.fetch(unstructured.groupVersionKind(), metadata.getName())
|
||||||
pluginWrapper.getRuntimeMode())
|
.flatMap(extension -> {
|
||||||
.stream()
|
unstructured.getMetadata()
|
||||||
.map(resourceLoader::getResource)
|
.setVersion(extension.getMetadata().getVersion());
|
||||||
.filter(Resource::exists)
|
return client.update(unstructured);
|
||||||
.map(resource -> new YamlUnstructuredLoader(resource).load())
|
})
|
||||||
.flatMap(List::stream)
|
.switchIfEmpty(Mono.defer(() -> client.create(unstructured)));
|
||||||
.forEach(unstructured -> {
|
}).then();
|
||||||
MetadataOperator metadata = unstructured.getMetadata();
|
}).then();
|
||||||
// collector plugin initialize extension resources
|
|
||||||
pluginApplicationContext.addExtensionMapping(unstructured.groupVersionKind(),
|
|
||||||
metadata.getName());
|
|
||||||
Map<String, String> labels = metadata.getLabels();
|
|
||||||
if (labels == null) {
|
|
||||||
labels = new HashMap<>();
|
|
||||||
metadata.setLabels(labels);
|
|
||||||
}
|
|
||||||
labels.put(PluginConst.PLUGIN_NAME_LABEL_NAME, plugin.getMetadata().getName());
|
|
||||||
extensionClient.fetch(unstructured.groupVersionKind(), metadata.getName())
|
|
||||||
.ifPresentOrElse(persisted -> {
|
|
||||||
unstructured.getMetadata().setVersion(persisted.getMetadata().getVersion());
|
|
||||||
extensionClient.update(unstructured);
|
|
||||||
}, () -> extensionClient.create(unstructured));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Set<String> lookupExtensions(Path pluginPath, RuntimeMode runtimeMode) {
|
Set<String> lookupExtensions(Path pluginPath, RuntimeMode runtimeMode) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import run.halo.app.extension.DefaultSchemeManager;
|
import run.halo.app.extension.DefaultSchemeManager;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>This {@link SharedApplicationContextHolder} class is used to hold a singleton instance of
|
* <p>This {@link SharedApplicationContextHolder} class is used to hold a singleton instance of
|
||||||
|
@ -52,8 +53,11 @@ public class SharedApplicationContextHolder {
|
||||||
(DefaultListableBeanFactory) sharedApplicationContext.getBeanFactory();
|
(DefaultListableBeanFactory) sharedApplicationContext.getBeanFactory();
|
||||||
|
|
||||||
// register shared object here
|
// register shared object here
|
||||||
ExtensionClient extensionClient = rootApplicationContext.getBean(ExtensionClient.class);
|
var extensionClient = rootApplicationContext.getBean(ExtensionClient.class);
|
||||||
|
var reactiveExtensionClient = rootApplicationContext.getBean(ReactiveExtensionClient.class);
|
||||||
beanFactory.registerSingleton("extensionClient", extensionClient);
|
beanFactory.registerSingleton("extensionClient", extensionClient);
|
||||||
|
beanFactory.registerSingleton("reactiveExtensionClient", reactiveExtensionClient);
|
||||||
|
|
||||||
DefaultSchemeManager defaultSchemeManager =
|
DefaultSchemeManager defaultSchemeManager =
|
||||||
rootApplicationContext.getBean(DefaultSchemeManager.class);
|
rootApplicationContext.getBean(DefaultSchemeManager.class);
|
||||||
beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
beanFactory.registerSingleton("schemeManager", defaultSchemeManager);
|
||||||
|
|
|
@ -6,12 +6,11 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.function.Predicate;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.lang.NonNull;
|
import org.springframework.lang.NonNull;
|
||||||
import org.springframework.lang.Nullable;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.AntPathMatcher;
|
import org.springframework.util.AntPathMatcher;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
@ -19,10 +18,12 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
|
import run.halo.app.core.extension.ReverseProxy.FileReverseProxyProvider;
|
||||||
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
|
import run.halo.app.core.extension.ReverseProxy.ReverseProxyRule;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.utils.PathUtils;
|
import run.halo.app.infra.utils.PathUtils;
|
||||||
import run.halo.app.plugin.PluginApplicationContext;
|
import run.halo.app.plugin.PluginApplicationContext;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
@ -40,11 +41,11 @@ import run.halo.app.plugin.PluginConst;
|
||||||
public class ReverseProxyRouterFunctionFactory {
|
public class ReverseProxyRouterFunctionFactory {
|
||||||
private static final String REVERSE_PROXY_API_PREFIX = "/assets";
|
private static final String REVERSE_PROXY_API_PREFIX = "/assets";
|
||||||
|
|
||||||
private final ExtensionClient extensionClient;
|
private final ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
private final JsBundleRuleProvider jsBundleRuleProvider;
|
private final JsBundleRuleProvider jsBundleRuleProvider;
|
||||||
|
|
||||||
public ReverseProxyRouterFunctionFactory(ExtensionClient extensionClient,
|
public ReverseProxyRouterFunctionFactory(ReactiveExtensionClient extensionClient,
|
||||||
JsBundleRuleProvider jsBundleRuleProvider) {
|
JsBundleRuleProvider jsBundleRuleProvider) {
|
||||||
this.extensionClient = extensionClient;
|
this.extensionClient = extensionClient;
|
||||||
this.jsBundleRuleProvider = jsBundleRuleProvider;
|
this.jsBundleRuleProvider = jsBundleRuleProvider;
|
||||||
|
@ -59,57 +60,52 @@ public class ReverseProxyRouterFunctionFactory {
|
||||||
* @param pluginApplicationContext plugin application context
|
* @param pluginApplicationContext plugin application context
|
||||||
* @return A reverse proxy RouterFunction handle(nullable)
|
* @return A reverse proxy RouterFunction handle(nullable)
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
public RouterFunction<ServerResponse> create(
|
public Mono<RouterFunction<ServerResponse>> create(
|
||||||
PluginApplicationContext pluginApplicationContext) {
|
PluginApplicationContext pluginApplicationContext) {
|
||||||
String pluginId = pluginApplicationContext.getPluginId();
|
return createReverseProxyRouterFunction(pluginApplicationContext);
|
||||||
List<ReverseProxyRule> reverseProxyRules = getReverseProxyRules(pluginId);
|
|
||||||
return createReverseProxyRouterFunction(reverseProxyRules, pluginApplicationContext);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private RouterFunction<ServerResponse> createReverseProxyRouterFunction(
|
private Mono<RouterFunction<ServerResponse>> createReverseProxyRouterFunction(
|
||||||
List<ReverseProxyRule> rules, PluginApplicationContext pluginApplicationContext) {
|
PluginApplicationContext pluginApplicationContext) {
|
||||||
Assert.notNull(rules, "The reverseProxyRules must not be null.");
|
|
||||||
Assert.notNull(pluginApplicationContext, "The pluginApplicationContext must not be null.");
|
Assert.notNull(pluginApplicationContext, "The pluginApplicationContext must not be null.");
|
||||||
|
|
||||||
String pluginId = pluginApplicationContext.getPluginId();
|
var pluginId = pluginApplicationContext.getPluginId();
|
||||||
return rules.stream()
|
var rules = getReverseProxyRules(pluginId);
|
||||||
.map(rule -> {
|
|
||||||
String routePath = buildRoutePath(pluginId, rule);
|
return rules.map(rule -> {
|
||||||
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginId,
|
String routePath = buildRoutePath(pluginId, rule);
|
||||||
routePath);
|
log.debug("Plugin [{}] registered reverse proxy route path [{}]", pluginId,
|
||||||
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
|
routePath);
|
||||||
request -> {
|
return RouterFunctions.route(GET(routePath).and(accept(ALL)),
|
||||||
Resource resource =
|
request -> {
|
||||||
loadResourceByFileRule(pluginApplicationContext, rule, request);
|
Resource resource =
|
||||||
if (!resource.exists()) {
|
loadResourceByFileRule(pluginApplicationContext, rule, request);
|
||||||
return ServerResponse.notFound().build();
|
if (!resource.exists()) {
|
||||||
}
|
return ServerResponse.notFound().build();
|
||||||
return ServerResponse.ok()
|
}
|
||||||
.bodyValue(resource);
|
return ServerResponse.ok()
|
||||||
});
|
.bodyValue(resource);
|
||||||
})
|
});
|
||||||
.reduce(RouterFunction::and)
|
}).reduce(RouterFunction::and);
|
||||||
.orElse(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ReverseProxyRule> getReverseProxyRules(String pluginId) {
|
private Flux<ReverseProxyRule> getReverseProxyRules(String pluginId) {
|
||||||
List<ReverseProxyRule> rules = extensionClient.list(ReverseProxy.class,
|
return extensionClient.list(ReverseProxy.class, hasPluginId(pluginId), null)
|
||||||
reverseProxy -> {
|
|
||||||
String pluginName = reverseProxy.getMetadata()
|
|
||||||
.getLabels()
|
|
||||||
.get(PluginConst.PLUGIN_NAME_LABEL_NAME);
|
|
||||||
return pluginId.equals(pluginName);
|
|
||||||
},
|
|
||||||
null)
|
|
||||||
.stream()
|
|
||||||
.map(ReverseProxy::getRules)
|
.map(ReverseProxy::getRules)
|
||||||
.flatMap(List::stream)
|
.flatMapIterable(rules -> rules)
|
||||||
.collect(Collectors.toList());
|
.concatWith(Flux.fromIterable(getJsBundleRules(pluginId)));
|
||||||
|
}
|
||||||
|
|
||||||
// populate plugin js bundle rules.
|
private Predicate<ReverseProxy> hasPluginId(String pluginId) {
|
||||||
rules.addAll(getJsBundleRules(pluginId));
|
return proxy -> {
|
||||||
return rules;
|
var labels = proxy.getMetadata().getLabels();
|
||||||
|
if (labels == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var pluginName = labels.get(PluginConst.PLUGIN_NAME_LABEL_NAME);
|
||||||
|
return pluginId.equals(pluginName);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ReverseProxyRule> getJsBundleRules(String pluginId) {
|
private List<ReverseProxyRule> getJsBundleRules(String pluginId) {
|
||||||
|
|
|
@ -26,13 +26,8 @@ public class DefaultUserDetailService
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
|
public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
|
||||||
return Mono.just(user)
|
return userService.updatePassword(user.getUsername(), newPassword)
|
||||||
.map(userDetails -> withNewPassword(user, newPassword))
|
.map(u -> withNewPassword(user, newPassword));
|
||||||
.flatMap(userDetails -> userService.updatePassword(
|
|
||||||
userDetails.getUsername(),
|
|
||||||
userDetails.getPassword())
|
|
||||||
.then(Mono.just(userDetails))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -7,9 +7,10 @@ import java.util.Map;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
import org.apache.commons.lang3.RandomStringUtils;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.ApplicationListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.Role.PolicyRule;
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
|
@ -17,39 +18,41 @@ import run.halo.app.core.extension.RoleBinding.RoleRef;
|
||||||
import run.halo.app.core.extension.RoleBinding.Subject;
|
import run.halo.app.core.extension.RoleBinding.Subject;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.core.extension.User.UserSpec;
|
import run.halo.app.core.extension.User.UserSpec;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.infra.properties.SecurityProperties.Initializer;
|
import run.halo.app.infra.properties.SecurityProperties.Initializer;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class SuperAdminInitializer implements ApplicationListener<ApplicationReadyEvent> {
|
public class SuperAdminInitializer {
|
||||||
|
|
||||||
private static final String SUPER_ROLE_NAME = "super-role";
|
private static final String SUPER_ROLE_NAME = "super-role";
|
||||||
|
|
||||||
private final ExtensionClient client;
|
private final ReactiveExtensionClient client;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
private final Initializer initializer;
|
private final Initializer initializer;
|
||||||
|
|
||||||
public SuperAdminInitializer(ExtensionClient client, PasswordEncoder passwordEncoder,
|
public SuperAdminInitializer(ReactiveExtensionClient client, PasswordEncoder passwordEncoder,
|
||||||
Initializer initializer) {
|
Initializer initializer) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.initializer = initializer;
|
this.initializer = initializer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@EventListener
|
||||||
public void onApplicationEvent(ApplicationReadyEvent event) {
|
public Mono<Void> initialize(ApplicationReadyEvent readyEvent) {
|
||||||
client.fetch(User.class, initializer.getSuperAdminUsername()).ifPresentOrElse(user -> {
|
return client.fetch(User.class, initializer.getSuperAdminUsername())
|
||||||
// do nothing if admin has been initialized
|
.switchIfEmpty(Mono.defer(() -> client.create(createAdmin()))
|
||||||
}, () -> {
|
.flatMap(admin -> {
|
||||||
var admin = createAdmin();
|
var superRole = createSuperRole();
|
||||||
var superRole = createSuperRole();
|
return client.create(superRole)
|
||||||
var roleBinding = bindAdminAndSuperRole(admin, superRole);
|
.flatMap(role -> {
|
||||||
client.create(admin);
|
var binding = bindAdminAndSuperRole(admin, superRole);
|
||||||
client.create(superRole);
|
return client.create(binding).thenReturn(role);
|
||||||
client.create(roleBinding);
|
})
|
||||||
});
|
.thenReturn(admin);
|
||||||
|
}))
|
||||||
|
.then();
|
||||||
}
|
}
|
||||||
|
|
||||||
RoleBinding bindAdminAndSuperRole(User admin, Role superRole) {
|
RoleBinding bindAdminAndSuperRole(User admin, Role superRole) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.security.authorization;
|
package run.halo.app.security.authorization;
|
||||||
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author guqing
|
* @author guqing
|
||||||
|
@ -17,6 +18,7 @@ public interface AuthorizationRuleResolver {
|
||||||
*
|
*
|
||||||
* @param user authenticated user info
|
* @param user authenticated user info
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "2.0.0")
|
||||||
PolicyRuleList rulesFor(UserDetails user);
|
PolicyRuleList rulesFor(UserDetails user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -27,5 +29,8 @@ public interface AuthorizationRuleResolver {
|
||||||
* @param user user info
|
* @param user user info
|
||||||
* @param visitor visitor
|
* @param visitor visitor
|
||||||
*/
|
*/
|
||||||
|
@Deprecated(forRemoval = true, since = "2.0.0")
|
||||||
void visitRulesFor(UserDetails user, RuleAccumulator visitor);
|
void visitRulesFor(UserDetails user, RuleAccumulator visitor);
|
||||||
|
|
||||||
|
Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,13 @@ import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
import run.halo.app.core.extension.service.DefaultRoleBindingService;
|
||||||
import run.halo.app.core.extension.service.RoleBindingService;
|
import run.halo.app.core.extension.service.RoleBindingService;
|
||||||
|
@ -77,6 +80,39 @@ public class DefaultRuleResolver implements AuthorizationRuleResolver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<AuthorizingVisitor> visitRules(UserDetails user, RequestInfo requestInfo) {
|
||||||
|
var roleNamesImmutable = roleBindingService.listBoundRoleNames(user.getAuthorities());
|
||||||
|
var roleNames = new HashSet<>(roleNamesImmutable);
|
||||||
|
roleNames.add(AUTHENTICATED_ROLE);
|
||||||
|
|
||||||
|
var record = new AttributesRecord(user, requestInfo);
|
||||||
|
var visitor = new AuthorizingVisitor(record);
|
||||||
|
var stopVisiting = new AtomicBoolean(false);
|
||||||
|
return Flux.fromIterable(roleNames)
|
||||||
|
.flatMap(roleName -> {
|
||||||
|
if (stopVisiting.get()) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return roleService.getMonoRole(roleName)
|
||||||
|
.onErrorResume(t -> visitor.visit(null, null, t), t -> {
|
||||||
|
//Do nothing here
|
||||||
|
return Mono.empty();
|
||||||
|
})
|
||||||
|
.doOnNext(role -> {
|
||||||
|
var rules = fetchRules(role);
|
||||||
|
var source = roleBindingDescriber(roleName, user.getUsername());
|
||||||
|
for (var rule : rules) {
|
||||||
|
if (!visitor.visit(source, rule, null)) {
|
||||||
|
stopVisiting.set(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(Mono.just(visitor));
|
||||||
|
}
|
||||||
|
|
||||||
private List<Role.PolicyRule> fetchRules(Role role) {
|
private List<Role.PolicyRule> fetchRules(Role role) {
|
||||||
MetadataOperator metadata = role.getMetadata();
|
MetadataOperator metadata = role.getMetadata();
|
||||||
if (metadata == null || metadata.getAnnotations() == null) {
|
if (metadata == null || metadata.getAnnotations() == null) {
|
||||||
|
|
|
@ -34,16 +34,16 @@ public class RequestInfoAuthorizationManager
|
||||||
ServerHttpRequest request = context.getExchange().getRequest();
|
ServerHttpRequest request = context.getExchange().getRequest();
|
||||||
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
RequestInfo requestInfo = RequestInfoFactory.INSTANCE.newRequestInfo(request);
|
||||||
|
|
||||||
return authentication.map(auth -> {
|
return authentication.flatMap(auth -> {
|
||||||
UserDetails userDetails = this.createUserDetails(auth);
|
var userDetails = this.createUserDetails(auth);
|
||||||
var record = new AttributesRecord(userDetails, requestInfo);
|
return this.ruleResolver.visitRules(userDetails, requestInfo)
|
||||||
var visitor = new AuthorizingVisitor(record);
|
.map(visitor -> {
|
||||||
ruleResolver.visitRulesFor(userDetails, visitor);
|
if (!visitor.isAllowed()) {
|
||||||
if (!visitor.isAllowed()) {
|
showErrorMessage(visitor.getErrors());
|
||||||
showErrorMessage(visitor.getErrors());
|
return new AuthorizationDecision(false);
|
||||||
return new AuthorizationDecision(false);
|
}
|
||||||
}
|
return new AuthorizationDecision(isGranted(auth));
|
||||||
return new AuthorizationDecision(isGranted(auth));
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.thymeleaf.spring6.ISpringWebFluxTemplateEngine;
|
|
||||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveView;
|
||||||
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
import org.thymeleaf.spring6.view.reactive.ThymeleafReactiveViewResolver;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
@ -28,17 +27,11 @@ public class HaloViewResolver extends ThymeleafReactiveViewResolver {
|
||||||
@Override
|
@Override
|
||||||
public Mono<Void> render(Map<String, ?> model, MediaType contentType,
|
public Mono<Void> render(Map<String, ?> model, MediaType contentType,
|
||||||
ServerWebExchange exchange) {
|
ServerWebExchange exchange) {
|
||||||
// calculate the engine before rendering
|
return themeResolver.getTheme(exchange.getRequest()).flatMap(theme -> {
|
||||||
var theme = themeResolver.getTheme(exchange.getRequest());
|
// calculate the engine before rendering
|
||||||
var templateEngine = engineManager.getTemplateEngine(theme);
|
setTemplateEngine(engineManager.getTemplateEngine(theme));
|
||||||
setTemplateEngine(templateEngine);
|
return super.render(model, contentType, exchange);
|
||||||
|
});
|
||||||
return super.render(model, contentType, exchange);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ISpringWebFluxTemplateEngine getTemplateEngine() {
|
|
||||||
return super.getTemplateEngine();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package run.halo.app.theme;
|
package run.halo.app.theme;
|
||||||
|
|
||||||
import java.util.function.Function;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||||
import run.halo.app.infra.SystemSetting.Theme;
|
import run.halo.app.infra.SystemSetting.Theme;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
|
@ -17,51 +17,37 @@ import run.halo.app.infra.utils.FilePathUtils;
|
||||||
public class ThemeResolver {
|
public class ThemeResolver {
|
||||||
private static final String THEME_WORK_DIR = "themes";
|
private static final String THEME_WORK_DIR = "themes";
|
||||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||||
private Function<ServerHttpRequest, ThemeContext> themeContextFunction;
|
|
||||||
private final HaloProperties haloProperties;
|
private final HaloProperties haloProperties;
|
||||||
|
|
||||||
public ThemeResolver(SystemConfigurableEnvironmentFetcher environmentFetcher,
|
public ThemeResolver(SystemConfigurableEnvironmentFetcher environmentFetcher,
|
||||||
HaloProperties haloProperties) {
|
HaloProperties haloProperties) {
|
||||||
this.environmentFetcher = environmentFetcher;
|
this.environmentFetcher = environmentFetcher;
|
||||||
this.haloProperties = haloProperties;
|
this.haloProperties = haloProperties;
|
||||||
themeContextFunction = this::defaultThemeContextFunction;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public ThemeContext getTheme(ServerHttpRequest request) {
|
public Mono<ThemeContext> getTheme(ServerHttpRequest request) {
|
||||||
return themeContextFunction.apply(request);
|
return environmentFetcher.fetch(Theme.GROUP, Theme.class)
|
||||||
}
|
|
||||||
|
|
||||||
private ThemeContext defaultThemeContextFunction(ServerHttpRequest request) {
|
|
||||||
var builder = ThemeContext.builder();
|
|
||||||
var themeName = request.getQueryParams().getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);
|
|
||||||
// TODO Fetch activated theme name from other place.
|
|
||||||
String activation = environmentFetcher.fetch(Theme.GROUP, Theme.class)
|
|
||||||
.map(Theme::getActive)
|
.map(Theme::getActive)
|
||||||
.orElseThrow();
|
.switchIfEmpty(Mono.error(() -> new IllegalArgumentException("No theme activated")))
|
||||||
if (StringUtils.isBlank(themeName)) {
|
.map(activatedTheme -> {
|
||||||
themeName = activation;
|
var builder = ThemeContext.builder();
|
||||||
}
|
var themeName =
|
||||||
if (StringUtils.equals(activation, themeName)) {
|
request.getQueryParams().getFirst(ThemeContext.THEME_PREVIEW_PARAM_NAME);
|
||||||
builder.active(true);
|
if (StringUtils.isBlank(themeName)) {
|
||||||
}
|
themeName = activatedTheme;
|
||||||
|
}
|
||||||
// TODO Validate the existence of the theme name.
|
boolean active = false;
|
||||||
|
if (StringUtils.equals(activatedTheme, themeName)) {
|
||||||
var path = FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
|
active = true;
|
||||||
THEME_WORK_DIR, themeName);
|
}
|
||||||
|
var path = FilePathUtils.combinePath(haloProperties.getWorkDir().toString(),
|
||||||
return builder
|
THEME_WORK_DIR, themeName);
|
||||||
.name(themeName)
|
return builder.name(themeName)
|
||||||
.path(path)
|
.path(path)
|
||||||
.build();
|
.active(active)
|
||||||
|
.build();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Function<ServerHttpRequest, ThemeContext> getThemeContextFunction() {
|
|
||||||
return themeContextFunction;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setThemeContextFunction(
|
|
||||||
Function<ServerHttpRequest, ThemeContext> themeContextFunction) {
|
|
||||||
this.themeContextFunction = themeContextFunction;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,6 @@ spring:
|
||||||
output:
|
output:
|
||||||
ansi:
|
ansi:
|
||||||
enabled: always
|
enabled: always
|
||||||
jpa:
|
|
||||||
show-sql: true
|
|
||||||
|
|
||||||
halo:
|
halo:
|
||||||
security:
|
security:
|
||||||
|
@ -25,7 +23,7 @@ halo:
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
run.halo.app: DEBUG
|
run.halo.app: DEBUG
|
||||||
|
org.springframework.r2dbc: DEBUG
|
||||||
springdoc:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
|
@ -4,30 +4,14 @@ spring:
|
||||||
output:
|
output:
|
||||||
ansi:
|
ansi:
|
||||||
enabled: detect
|
enabled: detect
|
||||||
datasource:
|
r2dbc:
|
||||||
type: com.zaxxer.hikari.HikariDataSource
|
url: r2dbc:h2:file:///${halo.work-dir}/db/halo-next?options=AUTO_SERVER=TRUE;MODE=MySQL
|
||||||
|
|
||||||
# H2 database configuration.
|
|
||||||
driver-class-name: org.h2.Driver
|
|
||||||
url: jdbc:h2:file:${halo.work-dir}/db/halo;AUTO_SERVER=TRUE
|
|
||||||
username: admin
|
username: admin
|
||||||
password: 123456
|
password: 123456
|
||||||
|
sql:
|
||||||
# MySQL database configuration.
|
init:
|
||||||
# driver-class-name: com.mysql.cj.jdbc.Driver
|
mode: always
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/halo_next?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
platform: h2
|
||||||
# username: root
|
|
||||||
# password: 123456
|
|
||||||
|
|
||||||
# PostgreSQL database configuration.
|
|
||||||
# driver-class-name: org.postgresql.Driver
|
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/halo_next?charSet=UTF8&ssl=false
|
|
||||||
# username: postgres
|
|
||||||
# password: 123456
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: update
|
|
||||||
open-in-view: false
|
|
||||||
|
|
||||||
halo:
|
halo:
|
||||||
security:
|
security:
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
create table if not exists extensions
|
||||||
|
(
|
||||||
|
name varchar(255) not null,
|
||||||
|
data blob,
|
||||||
|
version bigint,
|
||||||
|
primary key (name)
|
||||||
|
);
|
|
@ -2,10 +2,13 @@ package run.halo.app;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
import org.springframework.web.method.HandlerTypePredicate;
|
import org.springframework.web.method.HandlerTypePredicate;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test case for api path prefix predicate.
|
* Test case for api path prefix predicate.
|
||||||
|
@ -34,4 +37,14 @@ public class PathPrefixPredicateTest {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void test() {
|
||||||
|
Flux.fromIterable(List.of(1, 2, 3))
|
||||||
|
.flatMap(i -> {
|
||||||
|
if (i == 2) {
|
||||||
|
return Mono.empty();
|
||||||
|
}
|
||||||
|
return Mono.just(i);
|
||||||
|
}).subscribe(System.out::println);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
@ -54,7 +55,7 @@ class ExtensionConfigurationTest {
|
||||||
.build();
|
.build();
|
||||||
var role = new Role();
|
var role = new Role();
|
||||||
role.setRules(List.of(rule));
|
role.setRules(List.of(rule));
|
||||||
when(roleService.getRole(anyString())).thenReturn(role);
|
when(roleService.getMonoRole(anyString())).thenReturn(Mono.just(role));
|
||||||
|
|
||||||
// register scheme
|
// register scheme
|
||||||
schemeManager.register(FakeExtension.class);
|
schemeManager.register(FakeExtension.class);
|
||||||
|
@ -62,7 +63,7 @@ class ExtensionConfigurationTest {
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void cleanUp(@Autowired ExtensionStoreRepository repository) {
|
void cleanUp(@Autowired ExtensionStoreRepository repository) {
|
||||||
repository.deleteAll();
|
repository.deleteAll().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -2,7 +2,6 @@ package run.halo.app.core.extension.endpoint;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
@ -11,7 +10,6 @@ import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -25,8 +23,9 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import org.springframework.util.FileSystemUtils;
|
import org.springframework.util.FileSystemUtils;
|
||||||
import org.springframework.util.ResourceUtils;
|
import org.springframework.util.ResourceUtils;
|
||||||
import org.springframework.web.reactive.function.BodyInserters;
|
import org.springframework.web.reactive.function.BodyInserters;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Theme;
|
import run.halo.app.core.extension.Theme;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
|
@ -41,7 +40,7 @@ import run.halo.app.infra.utils.YamlUnstructuredLoader;
|
||||||
class ThemeEndpointTest {
|
class ThemeEndpointTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExtensionClient extensionClient;
|
private ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private HaloProperties haloProperties;
|
private HaloProperties haloProperties;
|
||||||
|
@ -74,18 +73,14 @@ class ThemeEndpointTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void install() {
|
void install() {
|
||||||
when(extensionClient.fetch(eq(Theme.class), eq("default")))
|
when(extensionClient.create(any(Unstructured.class))).thenReturn(
|
||||||
.then(answer -> {
|
Mono.fromCallable(() -> {
|
||||||
Path defaultThemeManifestPath = tmpHaloWorkDir.resolve("themes/default/theme.yaml");
|
var defaultThemeManifestPath = tmpHaloWorkDir.resolve("themes/default/theme.yaml");
|
||||||
assertThat(Files.exists(defaultThemeManifestPath)).isTrue();
|
assertThat(Files.exists(defaultThemeManifestPath)).isTrue();
|
||||||
|
return new YamlUnstructuredLoader(new FileSystemResource(defaultThemeManifestPath))
|
||||||
Unstructured unstructured =
|
.load()
|
||||||
new YamlUnstructuredLoader(new FileSystemResource(defaultThemeManifestPath))
|
.get(0);
|
||||||
.load()
|
})).thenReturn(Mono.empty()).thenReturn(Mono.empty());
|
||||||
.get(0);
|
|
||||||
return Optional.of(
|
|
||||||
Unstructured.OBJECT_MAPPER.convertValue(unstructured, Theme.class));
|
|
||||||
});
|
|
||||||
|
|
||||||
MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
|
MultipartBodyBuilder multipartBodyBuilder = new MultipartBodyBuilder();
|
||||||
multipartBodyBuilder.part("file", new FileSystemResource(defaultTheme))
|
multipartBodyBuilder.part("file", new FileSystemResource(defaultTheme))
|
||||||
|
|
|
@ -12,7 +12,6 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
@ -22,9 +21,11 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.security.test.context.support.WithMockUser;
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
|
@ -32,8 +33,9 @@ import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
import run.halo.app.core.extension.service.UserService;
|
import run.halo.app.core.extension.service.UserService;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -48,7 +50,7 @@ class UserEndpointTest {
|
||||||
RoleService roleService;
|
RoleService roleService;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@MockBean
|
@MockBean
|
||||||
UserService userService;
|
UserService userService;
|
||||||
|
@ -63,7 +65,7 @@ class UserEndpointTest {
|
||||||
.build();
|
.build();
|
||||||
var role = new Role();
|
var role = new Role();
|
||||||
role.setRules(List.of(rule));
|
role.setRules(List.of(rule));
|
||||||
when(roleService.getRole("fake-super-role")).thenReturn(role);
|
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.just(role));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
|
@ -72,10 +74,13 @@ class UserEndpointTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldResponseErrorIfUserNotFound() {
|
void shouldResponseErrorIfUserNotFound() {
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.empty());
|
when(client.get(User.class, "fake-user"))
|
||||||
|
.thenReturn(Mono.error(new ExtensionNotFoundException()));
|
||||||
webClient.get().uri("/apis/api.halo.run/v1alpha1/users/-")
|
webClient.get().uri("/apis/api.halo.run/v1alpha1/users/-")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().is5xxServerError();
|
.expectStatus().is5xxServerError();
|
||||||
|
|
||||||
|
verify(client).get(User.class, "fake-user");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -84,7 +89,7 @@ class UserEndpointTest {
|
||||||
metadata.setName("fake-user");
|
metadata.setName("fake-user");
|
||||||
var user = new User();
|
var user = new User();
|
||||||
user.setMetadata(metadata);
|
user.setMetadata(metadata);
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(user));
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(user));
|
||||||
webClient.get().uri("/apis/api.halo.run/v1alpha1/users/-")
|
webClient.get().uri("/apis/api.halo.run/v1alpha1/users/-")
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk()
|
.expectStatus().isOk()
|
||||||
|
@ -135,6 +140,13 @@ class UserEndpointTest {
|
||||||
@DisplayName("GrantPermission")
|
@DisplayName("GrantPermission")
|
||||||
class GrantPermissionEndpointTest {
|
class GrantPermissionEndpointTest {
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(Flux.empty());
|
||||||
|
when(client.get(User.class, "fake-user"))
|
||||||
|
.thenReturn(Mono.error(new ExtensionNotFoundException()));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetBadRequestIfRequestBodyIsEmpty() {
|
void shouldGetBadRequestIfRequestBodyIsEmpty() {
|
||||||
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||||
|
@ -149,8 +161,9 @@ class UserEndpointTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetNotFoundIfUserNotFound() {
|
void shouldGetNotFoundIfUserNotFound() {
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.empty());
|
when(client.get(User.class, "fake-user"))
|
||||||
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(mock(Role.class)));
|
.thenReturn(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
|
||||||
|
when(client.get(Role.class, "fake-role")).thenReturn(Mono.just(mock(Role.class)));
|
||||||
|
|
||||||
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
@ -158,14 +171,15 @@ class UserEndpointTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isNotFound();
|
.expectStatus().isNotFound();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
|
verify(client, times(1)).get(same(User.class), eq("fake-user"));
|
||||||
verify(client, never()).fetch(same(Role.class), eq("fake-role"));
|
verify(client, never()).get(same(Role.class), eq("fake-role"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetNotFoundIfRoleNotFound() {
|
void shouldGetNotFoundIfRoleNotFound() {
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(mock(User.class)));
|
||||||
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.empty());
|
when(client.get(Role.class, "fake-role"))
|
||||||
|
.thenReturn(Mono.error(new ResponseStatusException(HttpStatus.NOT_FOUND)));
|
||||||
|
|
||||||
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
@ -173,15 +187,15 @@ class UserEndpointTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isNotFound();
|
.expectStatus().isNotFound();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
|
verify(client).get(User.class, "fake-user");
|
||||||
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
|
verify(client).get(Role.class, "fake-role");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateRoleBindingIfNotExist() {
|
void shouldCreateRoleBindingIfNotExist() {
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(mock(User.class)));
|
||||||
var role = mock(Role.class);
|
var role = mock(Role.class);
|
||||||
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role));
|
when(client.get(Role.class, "fake-role")).thenReturn(Mono.just(role));
|
||||||
|
|
||||||
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
@ -189,31 +203,31 @@ class UserEndpointTest {
|
||||||
.exchange()
|
.exchange()
|
||||||
.expectStatus().isOk();
|
.expectStatus().isOk();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
|
verify(client).get(User.class, "fake-user");
|
||||||
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
|
verify(client).get(Role.class, "fake-role");
|
||||||
verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role"));
|
verify(client).create(RoleBinding.create("fake-user", "fake-role"));
|
||||||
verify(client, never()).update(isA(RoleBinding.class));
|
verify(client, never()).update(isA(RoleBinding.class));
|
||||||
verify(client, never()).delete(isA(RoleBinding.class));
|
verify(client, never()).delete(isA(RoleBinding.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDeleteRoleBindingIfNotProvided() {
|
void shouldDeleteRoleBindingIfNotProvided() {
|
||||||
when(client.fetch(User.class, "fake-user")).thenReturn(Optional.of(mock(User.class)));
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(mock(User.class)));
|
||||||
var role = mock(Role.class);
|
var role = mock(Role.class);
|
||||||
when(client.fetch(Role.class, "fake-role")).thenReturn(Optional.of(role));
|
when(client.get(Role.class, "fake-role")).thenReturn(Mono.just(role));
|
||||||
var roleBinding = RoleBinding.create("fake-user", "non-provided-fake-role");
|
var roleBinding = RoleBinding.create("fake-user", "non-provided-fake-role");
|
||||||
when(client.list(same(RoleBinding.class), any(), any())).thenReturn(
|
when(client.list(same(RoleBinding.class), any(), any()))
|
||||||
List.of(roleBinding));
|
.thenReturn(Flux.fromIterable(List.of(roleBinding)));
|
||||||
|
|
||||||
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
webClient.post().uri("/apis/api.halo.run/v1alpha1/users/fake-user/permissions")
|
||||||
.contentType(MediaType.APPLICATION_JSON)
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))).exchange()
|
.bodyValue(new UserEndpoint.GrantRequest(Set.of("fake-role"))).exchange()
|
||||||
.expectStatus().isOk();
|
.expectStatus().isOk();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(same(User.class), eq("fake-user"));
|
verify(client).get(User.class, "fake-user");
|
||||||
verify(client, times(1)).fetch(same(Role.class), eq("fake-role"));
|
verify(client).get(Role.class, "fake-role");
|
||||||
verify(client, times(1)).create(RoleBinding.create("fake-user", "fake-role"));
|
verify(client).create(RoleBinding.create("fake-user", "fake-role"));
|
||||||
verify(client, times(1))
|
verify(client)
|
||||||
.delete(argThat(binding -> binding.getMetadata().getName()
|
.delete(argThat(binding -> binding.getMetadata().getName()
|
||||||
.equals(roleBinding.getMetadata().getName())));
|
.equals(roleBinding.getMetadata().getName())));
|
||||||
verify(client, never()).update(isA(RoleBinding.class));
|
verify(client, never()).update(isA(RoleBinding.class));
|
||||||
|
|
|
@ -10,16 +10,16 @@ import static org.mockito.Mockito.when;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.TestRole;
|
import run.halo.app.core.extension.TestRole;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link DefaultRoleService}.
|
* Tests for {@link DefaultRoleService}.
|
||||||
|
@ -30,7 +30,7 @@ import run.halo.app.extension.ExtensionClient;
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class DefaultRoleServiceTest {
|
class DefaultRoleServiceTest {
|
||||||
@Mock
|
@Mock
|
||||||
private ExtensionClient extensionClient;
|
private ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
private DefaultRoleService roleService;
|
private DefaultRoleService roleService;
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class DefaultRoleServiceTest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void listDependencie() {
|
void listDependencies() {
|
||||||
Role roleManage = TestRole.getRoleManage();
|
Role roleManage = TestRole.getRoleManage();
|
||||||
Map<String, String> manageAnnotations = new HashMap<>();
|
Map<String, String> manageAnnotations = new HashMap<>();
|
||||||
manageAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
manageAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||||
|
@ -54,11 +54,11 @@ class DefaultRoleServiceTest {
|
||||||
Role roleOther = TestRole.getRoleOther();
|
Role roleOther = TestRole.getRoleOther();
|
||||||
|
|
||||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-manage")))
|
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-manage")))
|
||||||
.thenReturn(Optional.of(roleManage));
|
.thenReturn(Mono.just(roleManage));
|
||||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-view")))
|
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-view")))
|
||||||
.thenReturn(Optional.of(roleView));
|
.thenReturn(Mono.just(roleView));
|
||||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
||||||
.thenReturn(Optional.of(roleOther));
|
.thenReturn(Mono.just(roleOther));
|
||||||
|
|
||||||
// list without cycle
|
// list without cycle
|
||||||
List<Role> roles = roleService.listDependencies(Set.of("role-template-apple-manage"));
|
List<Role> roles = roleService.listDependencies(Set.of("role-template-apple-manage"));
|
||||||
|
@ -75,7 +75,7 @@ class DefaultRoleServiceTest {
|
||||||
anotherAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
anotherAnnotations.put(Role.ROLE_DEPENDENCIES_ANNO, "[\"role-template-apple-view\"]");
|
||||||
roleOther.getMetadata().setAnnotations(anotherAnnotations);
|
roleOther.getMetadata().setAnnotations(anotherAnnotations);
|
||||||
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
when(extensionClient.fetch(same(Role.class), eq("role-template-apple-other")))
|
||||||
.thenReturn(Optional.of(roleOther));
|
.thenReturn(Mono.just(roleOther));
|
||||||
// correct behavior is to ignore the cycle relation
|
// correct behavior is to ignore the cycle relation
|
||||||
List<Role> rolesFromCycle =
|
List<Role> rolesFromCycle =
|
||||||
roleService.listDependencies(Set.of("role-template-apple-manage"));
|
roleService.listDependencies(Set.of("role-template-apple-manage"));
|
||||||
|
|
|
@ -5,7 +5,6 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.argThat;
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.doReturn;
|
|
||||||
import static org.mockito.Mockito.lenient;
|
import static org.mockito.Mockito.lenient;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
import static org.mockito.Mockito.spy;
|
import static org.mockito.Mockito.spy;
|
||||||
|
@ -14,7 +13,6 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
@ -23,20 +21,22 @@ import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class UserServiceImplTest {
|
class UserServiceImplTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
PasswordEncoder passwordEncoder;
|
PasswordEncoder passwordEncoder;
|
||||||
|
@ -45,55 +45,47 @@ class UserServiceImplTest {
|
||||||
UserServiceImpl userService;
|
UserServiceImpl userService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetEmptyUserIfUserNotFoundInExtension() {
|
void shouldThrowExceptionIfUserNotFoundInExtension() {
|
||||||
when(client.fetch(User.class, "faker")).thenReturn(Optional.empty());
|
when(client.get(User.class, "faker")).thenReturn(
|
||||||
|
Mono.error(new ExtensionNotFoundException()));
|
||||||
|
|
||||||
StepVerifier.create(userService.getUser("faker"))
|
StepVerifier.create(userService.getUser("faker"))
|
||||||
.verifyComplete();
|
.verifyError(ExtensionNotFoundException.class);
|
||||||
|
|
||||||
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
verify(client, times(1)).get(eq(User.class), eq("faker"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldGetUserIfUserFoundInExtension() {
|
void shouldGetUserIfUserFoundInExtension() {
|
||||||
User fakeUser = new User();
|
User fakeUser = new User();
|
||||||
when(client.fetch(User.class, "faker")).thenReturn(Optional.of(fakeUser));
|
when(client.get(User.class, "faker")).thenReturn(Mono.just(fakeUser));
|
||||||
|
|
||||||
StepVerifier.create(userService.getUser("faker"))
|
StepVerifier.create(userService.getUser("faker"))
|
||||||
.assertNext(user -> assertEquals(fakeUser, user))
|
.assertNext(user -> assertEquals(fakeUser, user))
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
verify(client, times(1)).get(eq(User.class), eq("faker"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUpdatePasswordIfUserFoundInExtension() {
|
void shouldUpdatePasswordIfUserFoundInExtension() {
|
||||||
User fakeUser = new User();
|
var fakeUser = new User();
|
||||||
fakeUser.setSpec(new User.UserSpec());
|
fakeUser.setSpec(new User.UserSpec());
|
||||||
|
|
||||||
when(client.fetch(User.class, "faker")).thenReturn(Optional.of(fakeUser));
|
when(client.get(User.class, "faker")).thenReturn(Mono.just(fakeUser));
|
||||||
|
when(client.update(eq(fakeUser))).thenReturn(Mono.just(fakeUser));
|
||||||
|
|
||||||
StepVerifier.create(userService.updatePassword("faker", "new-fake-password"))
|
StepVerifier.create(userService.updatePassword("faker", "new-fake-password"))
|
||||||
|
.expectNext(fakeUser)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
verify(client, times(1)).get(eq(User.class), eq("faker"));
|
||||||
verify(client, times(1)).update(argThat(extension -> {
|
verify(client, times(1)).update(argThat(extension -> {
|
||||||
var user = (User) extension;
|
var user = (User) extension;
|
||||||
return "new-fake-password".equals(user.getSpec().getPassword());
|
return "new-fake-password".equals(user.getSpec().getPassword());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldNotUpdatePasswordIfUserNotFoundInExtension() {
|
|
||||||
when(client.fetch(User.class, "faker")).thenReturn(Optional.empty());
|
|
||||||
|
|
||||||
StepVerifier.create(userService.updatePassword("faker", "new-fake-password"))
|
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
verify(client, times(1)).fetch(eq(User.class), eq("faker"));
|
|
||||||
verify(client, times(0)).update(any());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldListRolesIfUserFoundInExtension() {
|
void shouldListRolesIfUserFoundInExtension() {
|
||||||
User fakeUser = new User();
|
User fakeUser = new User();
|
||||||
|
@ -102,7 +94,8 @@ class UserServiceImplTest {
|
||||||
fakeUser.setMetadata(metadata);
|
fakeUser.setMetadata(metadata);
|
||||||
fakeUser.setSpec(new User.UserSpec());
|
fakeUser.setSpec(new User.UserSpec());
|
||||||
|
|
||||||
when(client.list(eq(RoleBinding.class), any(), any())).thenReturn(getRoleBindings());
|
when(client.list(eq(RoleBinding.class), any(), any())).thenReturn(
|
||||||
|
Flux.fromIterable(getRoleBindings()));
|
||||||
Role roleA = new Role();
|
Role roleA = new Role();
|
||||||
Metadata metadataA = new Metadata();
|
Metadata metadataA = new Metadata();
|
||||||
metadataA.setName("test-A");
|
metadataA.setName("test-A");
|
||||||
|
@ -118,9 +111,9 @@ class UserServiceImplTest {
|
||||||
metadataC.setName("ddd");
|
metadataC.setName("ddd");
|
||||||
roleC.setMetadata(metadataC);
|
roleC.setMetadata(metadataC);
|
||||||
|
|
||||||
when(client.fetch(eq(Role.class), eq("test-A"))).thenReturn(Optional.of(roleA));
|
when(client.fetch(eq(Role.class), eq("test-A"))).thenReturn(Mono.just(roleA));
|
||||||
when(client.fetch(eq(Role.class), eq("test-B"))).thenReturn(Optional.of(roleB));
|
when(client.fetch(eq(Role.class), eq("test-B"))).thenReturn(Mono.just(roleB));
|
||||||
lenient().when(client.fetch(eq(Role.class), eq("ddd"))).thenReturn(Optional.of(roleC));
|
lenient().when(client.fetch(eq(Role.class), eq("ddd"))).thenReturn(Mono.just(roleC));
|
||||||
|
|
||||||
StepVerifier.create(userService.listRoles("faker"))
|
StepVerifier.create(userService.listRoles("faker"))
|
||||||
.expectNext(roleA)
|
.expectNext(roleA)
|
||||||
|
@ -212,50 +205,58 @@ class UserServiceImplTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUpdatePasswordWithDifferentPassword() {
|
void shouldUpdatePasswordWithDifferentPassword() {
|
||||||
userService = spy(userService);
|
var oldUser = createUser("fake-password");
|
||||||
|
var newUser = createUser("new-password");
|
||||||
|
|
||||||
doReturn(
|
when(client.get(User.class, "fake-user")).thenReturn(
|
||||||
Mono.just(createUser("fake-password")),
|
Mono.just(oldUser));
|
||||||
Mono.just(createUser("new-password")))
|
when(client.update(eq(oldUser))).thenReturn(Mono.just(newUser));
|
||||||
.when(userService)
|
|
||||||
.getUser("fake-user");
|
|
||||||
when(passwordEncoder.matches("new-password", "fake-password")).thenReturn(false);
|
when(passwordEncoder.matches("new-password", "fake-password")).thenReturn(false);
|
||||||
|
when(passwordEncoder.encode("new-password")).thenReturn("encoded-new-password");
|
||||||
|
|
||||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||||
.expectNext(createUser("new-password"))
|
.expectNext(newUser)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(passwordEncoder, times(1)).matches("new-password", "fake-password");
|
verify(passwordEncoder).matches("new-password", "fake-password");
|
||||||
verify(passwordEncoder, times(1)).encode("new-password");
|
verify(passwordEncoder).encode("new-password");
|
||||||
verify(userService, times(2)).getUser("fake-user");
|
verify(client).get(User.class, "fake-user");
|
||||||
|
verify(client).update(argThat(extension -> {
|
||||||
|
var user = (User) extension;
|
||||||
|
return "encoded-new-password".equals(user.getSpec().getPassword());
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUpdatePasswordIfNoPasswordBefore() {
|
void shouldUpdatePasswordIfNoPasswordBefore() {
|
||||||
userService = spy(userService);
|
var oldUser = createUser("");
|
||||||
|
var newUser = createUser("new-password");
|
||||||
|
|
||||||
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(oldUser));
|
||||||
|
when(client.update(oldUser)).thenReturn(Mono.just(newUser));
|
||||||
|
when(passwordEncoder.matches("new-password", "")).thenReturn(false);
|
||||||
|
when(passwordEncoder.encode("new-password")).thenReturn("encoded-new-password");
|
||||||
|
|
||||||
doReturn(
|
|
||||||
Mono.just(createUser("")),
|
|
||||||
Mono.just(createUser("new-password")))
|
|
||||||
.when(userService)
|
|
||||||
.getUser("fake-user");
|
|
||||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||||
.expectNext(createUser("new-password"))
|
.expectNext(newUser)
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(passwordEncoder, never()).matches(anyString(), anyString());
|
verify(passwordEncoder).matches("new-password", "");
|
||||||
verify(passwordEncoder, times(1)).encode("new-password");
|
verify(passwordEncoder).encode("new-password");
|
||||||
verify(userService, times(2)).getUser("fake-user");
|
verify(client).update(argThat(extension -> {
|
||||||
|
var user = (User) extension;
|
||||||
|
return "encoded-new-password".equals(user.getSpec().getPassword());
|
||||||
|
}));
|
||||||
|
verify(client).get(User.class, "fake-user");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDoNothingIfPasswordNotChanged() {
|
void shouldDoNothingIfPasswordNotChanged() {
|
||||||
userService = spy(userService);
|
userService = spy(userService);
|
||||||
|
|
||||||
doReturn(
|
var oldUser = createUser("fake-password");
|
||||||
Mono.just(createUser("fake-password")),
|
var newUser = createUser("new-password");
|
||||||
Mono.just(createUser("new-password")))
|
when(client.get(User.class, "fake-user")).thenReturn(Mono.just(oldUser));
|
||||||
.when(userService)
|
|
||||||
.getUser("fake-user");
|
|
||||||
when(passwordEncoder.matches("fake-password", "fake-password")).thenReturn(true);
|
when(passwordEncoder.matches("fake-password", "fake-password")).thenReturn(true);
|
||||||
|
|
||||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "fake-password"))
|
StepVerifier.create(userService.updateWithRawPassword("fake-user", "fake-password"))
|
||||||
|
@ -263,22 +264,23 @@ class UserServiceImplTest {
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
|
|
||||||
verify(passwordEncoder, times(1)).matches("fake-password", "fake-password");
|
verify(passwordEncoder, times(1)).matches("fake-password", "fake-password");
|
||||||
verify(passwordEncoder, never()).encode("fake-password");
|
verify(passwordEncoder, never()).encode(any());
|
||||||
verify(userService, times(1)).getUser("fake-user");
|
verify(client, never()).update(any());
|
||||||
|
verify(client).get(User.class, "fake-user");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldDoNothingIfUserNotFound() {
|
void shouldThrowExceptionIfUserNotFound() {
|
||||||
userService = spy(userService);
|
when(client.get(User.class, "fake-user"))
|
||||||
|
.thenReturn(Mono.error(new ExtensionNotFoundException()));
|
||||||
|
|
||||||
doReturn(Mono.empty()).when(userService).getUser("fake-user");
|
|
||||||
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
StepVerifier.create(userService.updateWithRawPassword("fake-user", "new-password"))
|
||||||
.expectNextCount(0)
|
.verifyError(ExtensionNotFoundException.class);
|
||||||
.verifyComplete();
|
|
||||||
|
|
||||||
verify(passwordEncoder, never()).matches(anyString(), anyString());
|
verify(passwordEncoder, never()).matches(anyString(), anyString());
|
||||||
verify(passwordEncoder, never()).encode(anyString());
|
verify(passwordEncoder, never()).encode(anyString());
|
||||||
verify(userService, times(1)).getUser(anyString());
|
verify(client, never()).update(any());
|
||||||
|
verify(client).get(User.class, "fake-user");
|
||||||
}
|
}
|
||||||
|
|
||||||
User createUser(String password) {
|
User createUser(String password) {
|
||||||
|
|
|
@ -48,6 +48,9 @@ class DefaultExtensionClientTest {
|
||||||
@Mock
|
@Mock
|
||||||
SchemeManager schemeManager;
|
SchemeManager schemeManager;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ReactiveExtensionClient reactiveClient;
|
||||||
|
|
||||||
@InjectMocks
|
@InjectMocks
|
||||||
DefaultExtensionClient client;
|
DefaultExtensionClient client;
|
||||||
|
|
||||||
|
@ -361,6 +364,7 @@ class DefaultExtensionClientTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
client.watch(watcher);
|
client.watch(watcher);
|
||||||
|
verify(reactiveClient).watch(watcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -0,0 +1,441 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static java.util.Collections.emptyList;
|
||||||
|
import static java.util.Collections.reverseOrder;
|
||||||
|
import static java.util.Comparator.comparing;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static run.halo.app.extension.GroupVersionKind.fromAPIVersionAndKind;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Nested;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import run.halo.app.extension.exception.SchemeNotFoundException;
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
import run.halo.app.extension.store.ReactiveExtensionStoreClient;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ReactiveExtensionClientTest {
|
||||||
|
|
||||||
|
static final Scheme fakeScheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ReactiveExtensionStoreClient storeClient;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionConverter converter;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
SchemeManager schemeManager;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
ReactiveExtensionClientImpl client;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
lenient().when(schemeManager.get(eq(FakeExtension.class)))
|
||||||
|
.thenReturn(fakeScheme);
|
||||||
|
lenient().when(schemeManager.get(eq(fakeScheme.groupVersionKind()))).thenReturn(fakeScheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
FakeExtension createFakeExtension(String name, Long version) {
|
||||||
|
var fake = new FakeExtension();
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName(name);
|
||||||
|
metadata.setVersion(version);
|
||||||
|
|
||||||
|
fake.setMetadata(metadata);
|
||||||
|
fake.setApiVersion("fake.halo.run/v1alpha1");
|
||||||
|
fake.setKind("Fake");
|
||||||
|
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionStore createExtensionStore(String name) {
|
||||||
|
return createExtensionStore(name, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionStore createExtensionStore(String name, Long version) {
|
||||||
|
var extensionStore = new ExtensionStore();
|
||||||
|
extensionStore.setName(name);
|
||||||
|
extensionStore.setVersion(version);
|
||||||
|
return extensionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
Unstructured createUnstructured() throws JsonProcessingException {
|
||||||
|
String extensionJson = """
|
||||||
|
{
|
||||||
|
"apiVersion": "fake.halo.run/v1alpha1",
|
||||||
|
"kind": "Fake",
|
||||||
|
"metadata": {
|
||||||
|
"labels": {
|
||||||
|
"category": "fake",
|
||||||
|
"default": "true"
|
||||||
|
},
|
||||||
|
"name": "fake",
|
||||||
|
"creationTimestamp": "2011-12-03T10:15:30Z",
|
||||||
|
"version": 12345
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
return Unstructured.OBJECT_MAPPER.readValue(extensionJson, Unstructured.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSchemeNotFoundExceptionWhenSchemeNotRegistered() {
|
||||||
|
class UnRegisteredExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
when(schemeManager.get(eq(UnRegisteredExtension.class)))
|
||||||
|
.thenThrow(SchemeNotFoundException.class);
|
||||||
|
when(schemeManager.get(isA(GroupVersionKind.class)))
|
||||||
|
.thenThrow(SchemeNotFoundException.class);
|
||||||
|
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.list(UnRegisteredExtension.class, null, null));
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.list(UnRegisteredExtension.class, null, null, 0, 10));
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.fetch(UnRegisteredExtension.class, "fake"));
|
||||||
|
assertThrows(SchemeNotFoundException.class, () ->
|
||||||
|
client.fetch(fromAPIVersionAndKind("fake.halo.run/v1alpha1", "UnRegistered"), "fake"));
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> {
|
||||||
|
when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);
|
||||||
|
client.create(createFakeExtension("fake", null));
|
||||||
|
});
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> {
|
||||||
|
when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);
|
||||||
|
client.update(createFakeExtension("fake", 1L));
|
||||||
|
});
|
||||||
|
assertThrows(SchemeNotFoundException.class, () -> {
|
||||||
|
when(converter.convertTo(any())).thenThrow(SchemeNotFoundException.class);
|
||||||
|
client.delete(createFakeExtension("fake", 1L));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnEmptyExtensions() {
|
||||||
|
when(storeClient.listByNamePrefix(anyString())).thenReturn(Flux.empty());
|
||||||
|
var fakes = client.list(FakeExtension.class, null, null);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnExtensionsWithFilterAndSorter() {
|
||||||
|
var fake1 = createFakeExtension("fake-01", 1L);
|
||||||
|
var fake2 = createFakeExtension("fake-02", 1L);
|
||||||
|
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore("fake-01"))).thenReturn(
|
||||||
|
fake1);
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore("fake-02"))).thenReturn(
|
||||||
|
fake2);
|
||||||
|
when(storeClient.listByNamePrefix(anyString())).thenReturn(
|
||||||
|
Flux.fromIterable(
|
||||||
|
List.of(createExtensionStore("fake-01"), createExtensionStore("fake-02"))));
|
||||||
|
|
||||||
|
// without filter and sorter
|
||||||
|
var fakes = client.list(FakeExtension.class, null, null);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(fake1)
|
||||||
|
.expectNext(fake2)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// with filter
|
||||||
|
fakes = client.list(FakeExtension.class, fake -> {
|
||||||
|
String name = fake.getMetadata().getName();
|
||||||
|
return "fake-01".equals(name);
|
||||||
|
}, null);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(fake1)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// with sorter
|
||||||
|
fakes = client.list(FakeExtension.class, null,
|
||||||
|
reverseOrder(comparing(fake -> fake.getMetadata().getName())));
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(fake2)
|
||||||
|
.expectNext(fake1)
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldQueryPageableAndCorrectly() {
|
||||||
|
var fake1 = createFakeExtension("fake-01", 1L);
|
||||||
|
var fake2 = createFakeExtension("fake-02", 1L);
|
||||||
|
var fake3 = createFakeExtension("fake-03", 1L);
|
||||||
|
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore("fake-01"))).thenReturn(
|
||||||
|
fake1);
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore("fake-02"))).thenReturn(
|
||||||
|
fake2);
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore("fake-03"))).thenReturn(
|
||||||
|
fake3);
|
||||||
|
|
||||||
|
when(storeClient.listByNamePrefix(anyString())).thenReturn(Flux.fromIterable(
|
||||||
|
List.of(createExtensionStore("fake-01"), createExtensionStore("fake-02"),
|
||||||
|
createExtensionStore("fake-03"))));
|
||||||
|
|
||||||
|
// without filter and sorter.
|
||||||
|
var fakes = client.list(FakeExtension.class, null, null, 1, 10);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(new ListResult<>(1, 10, 3, List.of(fake1, fake2, fake3)))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// out of page range
|
||||||
|
fakes = client.list(FakeExtension.class, null, null, 100, 10);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(new ListResult<>(100, 10, 3, emptyList()))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// with filter only
|
||||||
|
fakes =
|
||||||
|
client.list(FakeExtension.class, fake -> "fake-03".equals(fake.getMetadata().getName()),
|
||||||
|
null, 1, 10);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(new ListResult<>(1, 10, 1, List.of(fake3)))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// with sorter only
|
||||||
|
fakes = client.list(FakeExtension.class, null,
|
||||||
|
reverseOrder(comparing(fake -> fake.getMetadata().getName())), 1, 10);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(new ListResult<>(1, 10, 3, List.of(fake3, fake2, fake1)))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
// without page
|
||||||
|
fakes = client.list(FakeExtension.class, null, null, 0, 0);
|
||||||
|
StepVerifier.create(fakes)
|
||||||
|
.expectNext(new ListResult<>(0, 0, 3, List.of(fake1, fake2, fake3)))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchNothing() {
|
||||||
|
when(storeClient.fetchByName(any())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
var fake = client.fetch(FakeExtension.class, "fake");
|
||||||
|
|
||||||
|
StepVerifier.create(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(0)).convertFrom(any(), any());
|
||||||
|
verify(storeClient, times(1)).fetchByName(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotFetchUnstructured() {
|
||||||
|
when(schemeManager.get(isA(GroupVersionKind.class)))
|
||||||
|
.thenReturn(fakeScheme);
|
||||||
|
when(storeClient.fetchByName(any())).thenReturn(Mono.empty());
|
||||||
|
var unstructuredFake = client.fetch(fakeScheme.groupVersionKind(), "fake");
|
||||||
|
StepVerifier.create(unstructuredFake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(0)).convertFrom(any(), any());
|
||||||
|
verify(schemeManager, times(1)).get(isA(GroupVersionKind.class));
|
||||||
|
verify(storeClient, times(1)).fetchByName(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchAnExtension() {
|
||||||
|
var storeName = "/registry/fake.halo.run/fakes/fake";
|
||||||
|
when(storeClient.fetchByName(storeName)).thenReturn(
|
||||||
|
Mono.just(createExtensionStore(storeName)));
|
||||||
|
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore(storeName))).thenReturn(
|
||||||
|
createFakeExtension("fake", 1L));
|
||||||
|
|
||||||
|
var fake = client.fetch(FakeExtension.class, "fake");
|
||||||
|
StepVerifier.create(fake)
|
||||||
|
.expectNext(createFakeExtension("fake", 1L))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(storeClient, times(1)).fetchByName(eq(storeName));
|
||||||
|
verify(converter, times(1)).convertFrom(eq(FakeExtension.class),
|
||||||
|
eq(createExtensionStore(storeName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchUnstructuredExtension() throws JsonProcessingException {
|
||||||
|
var storeName = "/registry/fake.halo.run/fakes/fake";
|
||||||
|
when(storeClient.fetchByName(storeName)).thenReturn(
|
||||||
|
Mono.just(createExtensionStore(storeName)));
|
||||||
|
when(schemeManager.get(isA(GroupVersionKind.class)))
|
||||||
|
.thenReturn(fakeScheme);
|
||||||
|
when(converter.convertFrom(Unstructured.class, createExtensionStore(storeName)))
|
||||||
|
.thenReturn(createUnstructured());
|
||||||
|
|
||||||
|
var fake = client.fetch(fakeScheme.groupVersionKind(), "fake");
|
||||||
|
StepVerifier.create(fake)
|
||||||
|
.expectNext(createUnstructured())
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(storeClient, times(1)).fetchByName(eq(storeName));
|
||||||
|
verify(schemeManager, times(1)).get(isA(GroupVersionKind.class));
|
||||||
|
verify(converter, times(1)).convertFrom(eq(Unstructured.class),
|
||||||
|
eq(createExtensionStore(storeName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateSuccessfully() {
|
||||||
|
var fake = createFakeExtension("fake", null);
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
when(storeClient.create(any(), any())).thenReturn(
|
||||||
|
Mono.just(createExtensionStore("/registry/fake.halo.run/fakes/fake")));
|
||||||
|
when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);
|
||||||
|
|
||||||
|
StepVerifier.create(client.create(fake))
|
||||||
|
.expectNext(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(eq(fake));
|
||||||
|
verify(storeClient, times(1)).create(eq("/registry/fake.halo.run/fakes/fake"), any());
|
||||||
|
assertNotNull(fake.getMetadata().getCreationTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldCreateUsingUnstructuredSuccessfully() throws JsonProcessingException {
|
||||||
|
var fake = createUnstructured();
|
||||||
|
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
when(storeClient.create(any(), any())).thenReturn(
|
||||||
|
Mono.just(createExtensionStore("/registry/fake.halo.run/fakes/fake")));
|
||||||
|
when(converter.convertFrom(same(Unstructured.class), any())).thenReturn(fake);
|
||||||
|
|
||||||
|
StepVerifier.create(client.create(fake))
|
||||||
|
.expectNext(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(eq(fake));
|
||||||
|
verify(storeClient, times(1)).create(eq("/registry/fake.halo.run/fakes/fake"), any());
|
||||||
|
assertNotNull(fake.getMetadata().getCreationTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateSuccessfully() {
|
||||||
|
var fake = createFakeExtension("fake", 2L);
|
||||||
|
var storeName = "/registry/fake.halo.run/fakes/fake";
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore(storeName, 2L));
|
||||||
|
when(storeClient.update(any(), any(), any())).thenReturn(
|
||||||
|
Mono.just(createExtensionStore(storeName, 2L)));
|
||||||
|
when(storeClient.fetchByName(storeName)).thenReturn(
|
||||||
|
Mono.just(createExtensionStore(storeName, 1L)));
|
||||||
|
when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);
|
||||||
|
|
||||||
|
StepVerifier.create(client.update(fake))
|
||||||
|
.expectNext(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(eq(fake));
|
||||||
|
verify(storeClient, times(1))
|
||||||
|
.update(eq("/registry/fake.halo.run/fakes/fake"), eq(2L), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateUnstructuredSuccessfully() throws JsonProcessingException {
|
||||||
|
var fake = createUnstructured();
|
||||||
|
var name = "/registry/fake.halo.run/fakes/fake";
|
||||||
|
when(converter.convertTo(any()))
|
||||||
|
.thenReturn(createExtensionStore(name, 12345L));
|
||||||
|
when(storeClient.update(any(), any(), any()))
|
||||||
|
.thenReturn(Mono.just(createExtensionStore(name, 12345L)));
|
||||||
|
when(storeClient.fetchByName(name))
|
||||||
|
.thenReturn(Mono.just(createExtensionStore(name, 12346L)));
|
||||||
|
when(converter.convertFrom(same(Unstructured.class), any())).thenReturn(fake);
|
||||||
|
|
||||||
|
StepVerifier.create(client.update(fake))
|
||||||
|
.expectNext(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(eq(fake));
|
||||||
|
verify(storeClient, times(1))
|
||||||
|
.update(eq("/registry/fake.halo.run/fakes/fake"), eq(12345L), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteSuccessfully() {
|
||||||
|
var fake = createFakeExtension("fake", 2L);
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
when(storeClient.update(any(), any(), any())).thenReturn(
|
||||||
|
Mono.just(createExtensionStore("/registry/fake.halo.run/fakes/fake")));
|
||||||
|
when(converter.convertFrom(same(FakeExtension.class), any())).thenReturn(fake);
|
||||||
|
|
||||||
|
StepVerifier.create(client.delete(fake))
|
||||||
|
.expectNext(fake)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(any());
|
||||||
|
verify(storeClient, times(1)).update(any(), any(), any());
|
||||||
|
verify(storeClient, never()).delete(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
@DisplayName("Extension watcher test")
|
||||||
|
class WatcherTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
Watcher watcher;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
client.watch(watcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWatchOnAddSuccessfully() {
|
||||||
|
doNothing().when(watcher).onAdd(any());
|
||||||
|
shouldCreateSuccessfully();
|
||||||
|
|
||||||
|
verify(watcher, times(1)).onAdd(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWatchOnUpdateSuccessfully() {
|
||||||
|
doNothing().when(watcher).onUpdate(any(), any());
|
||||||
|
shouldUpdateSuccessfully();
|
||||||
|
|
||||||
|
verify(watcher, times(1)).onUpdate(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldWatchOnDeleteSuccessfully() {
|
||||||
|
doNothing().when(watcher).onDelete(any());
|
||||||
|
shouldDeleteSuccessfully();
|
||||||
|
|
||||||
|
verify(watcher, times(1)).onDelete(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,8 +15,8 @@ import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||||
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.SchemeWatcherManager;
|
import run.halo.app.extension.SchemeWatcherManager;
|
||||||
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
|
import run.halo.app.extension.SchemeWatcherManager.SchemeRegistered;
|
||||||
|
@ -26,7 +26,7 @@ import run.halo.app.extension.SchemeWatcherManager.SchemeUnregistered;
|
||||||
class ExtensionCompositeRouterFunctionTest {
|
class ExtensionCompositeRouterFunctionTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldRouteWhenSchemeRegistered() {
|
void shouldRouteWhenSchemeRegistered() {
|
||||||
|
|
|
@ -13,7 +13,6 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
@ -24,9 +23,9 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
|
@ -36,7 +35,7 @@ import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
class ExtensionCreateHandlerTest {
|
class ExtensionCreateHandlerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildPathPatternCorrectly() {
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
@ -60,7 +59,7 @@ class ExtensionCreateHandlerTest {
|
||||||
|
|
||||||
var serverRequest = MockServerRequest.builder()
|
var serverRequest = MockServerRequest.builder()
|
||||||
.body(Mono.just(unstructured));
|
.body(Mono.just(unstructured));
|
||||||
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
when(client.create(any(Unstructured.class))).thenReturn(Mono.just(unstructured));
|
||||||
|
|
||||||
var scheme = Scheme.buildFromType(FakeExtension.class);
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
var getHandler = new ExtensionCreateHandler(scheme, client);
|
var getHandler = new ExtensionCreateHandler(scheme, client);
|
||||||
|
@ -70,13 +69,12 @@ class ExtensionCreateHandlerTest {
|
||||||
.consumeNextWith(response -> {
|
.consumeNextWith(response -> {
|
||||||
assertEquals(HttpStatus.CREATED, response.statusCode());
|
assertEquals(HttpStatus.CREATED, response.statusCode());
|
||||||
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/my-fake",
|
assertEquals("/apis/fake.halo.run/v1alpha1/fakes/my-fake",
|
||||||
response.headers().getLocation().toString());
|
Objects.requireNonNull(response.headers().getLocation()).toString());
|
||||||
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
||||||
assertTrue(response instanceof EntityResponse<?>);
|
assertTrue(response instanceof EntityResponse<?>);
|
||||||
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
assertEquals(unstructured, ((EntityResponse<?>) response).entity());
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
verify(client, times(1)).fetch(eq(FakeExtension.class), eq("my-fake"));
|
|
||||||
verify(client, times(1)).create(eq(unstructured));
|
verify(client, times(1)).create(eq(unstructured));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,10 @@ import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.ArgumentMatchers.same;
|
import static org.mockito.ArgumentMatchers.same;
|
||||||
import static org.mockito.Mockito.doNothing;
|
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
@ -23,9 +21,9 @@ import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
@ -34,7 +32,7 @@ import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
class ExtensionDeleteHandlerTest {
|
class ExtensionDeleteHandlerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildPathPatternCorrectly() {
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
@ -59,8 +57,8 @@ class ExtensionDeleteHandlerTest {
|
||||||
var serverRequest = MockServerRequest.builder()
|
var serverRequest = MockServerRequest.builder()
|
||||||
.pathVariable("name", "my-fake")
|
.pathVariable("name", "my-fake")
|
||||||
.body(Mono.just(unstructured));
|
.body(Mono.just(unstructured));
|
||||||
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
when(client.get(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Mono.just(fake));
|
||||||
doNothing().when(client).delete(any());
|
when(client.delete(eq(fake))).thenReturn(Mono.just(fake));
|
||||||
|
|
||||||
var scheme = Scheme.buildFromType(FakeExtension.class);
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
|
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
|
||||||
|
@ -74,7 +72,7 @@ class ExtensionDeleteHandlerTest {
|
||||||
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
verify(client, times(2)).fetch(eq(FakeExtension.class), eq("my-fake"));
|
verify(client, times(1)).get(eq(FakeExtension.class), eq("my-fake"));
|
||||||
verify(client, times(1)).delete(any());
|
verify(client, times(1)).delete(any());
|
||||||
verify(client, times(0)).update(any());
|
verify(client, times(0)).update(any());
|
||||||
}
|
}
|
||||||
|
@ -93,7 +91,8 @@ class ExtensionDeleteHandlerTest {
|
||||||
var serverRequest = MockServerRequest.builder()
|
var serverRequest = MockServerRequest.builder()
|
||||||
.pathVariable("name", "my-fake")
|
.pathVariable("name", "my-fake")
|
||||||
.build();
|
.build();
|
||||||
when(client.fetch(FakeExtension.class, "my-fake")).thenReturn(Optional.empty());
|
when(client.get(FakeExtension.class, "my-fake")).thenReturn(
|
||||||
|
Mono.error(new ExtensionNotFoundException()));
|
||||||
|
|
||||||
var scheme = Scheme.buildFromType(FakeExtension.class);
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
|
var deleteHandler = new ExtensionDeleteHandler(scheme, client);
|
||||||
|
@ -102,7 +101,7 @@ class ExtensionDeleteHandlerTest {
|
||||||
StepVerifier.create(responseMono)
|
StepVerifier.create(responseMono)
|
||||||
.verifyError(ExtensionNotFoundException.class);
|
.verifyError(ExtensionNotFoundException.class);
|
||||||
|
|
||||||
verify(client, times(1)).fetch(same(FakeExtension.class), anyString());
|
verify(client, times(1)).get(same(FakeExtension.class), anyString());
|
||||||
verify(client, times(0)).update(any());
|
verify(client, times(0)).update(any());
|
||||||
verify(client, times(0)).delete(any());
|
verify(client, times(0)).delete(any());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
package run.halo.app.extension.router;
|
package run.halo.app.extension.router;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
@ -15,9 +13,11 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
|
@ -25,7 +25,7 @@ import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
class ExtensionGetHandlerTest {
|
class ExtensionGetHandlerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildPathPatternCorrectly() {
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
@ -43,7 +43,7 @@ class ExtensionGetHandlerTest {
|
||||||
.pathVariable("name", "my-fake")
|
.pathVariable("name", "my-fake")
|
||||||
.build();
|
.build();
|
||||||
final var fake = new FakeExtension();
|
final var fake = new FakeExtension();
|
||||||
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
when(client.get(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Mono.just(fake));
|
||||||
|
|
||||||
var responseMono = getHandler.handle(serverRequest);
|
var responseMono = getHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
@ -64,8 +64,12 @@ class ExtensionGetHandlerTest {
|
||||||
var serverRequest = MockServerRequest.builder()
|
var serverRequest = MockServerRequest.builder()
|
||||||
.pathVariable("name", "my-fake")
|
.pathVariable("name", "my-fake")
|
||||||
.build();
|
.build();
|
||||||
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.empty());
|
when(client.get(eq(FakeExtension.class), eq("my-fake"))).thenReturn(
|
||||||
|
Mono.error(new ExtensionNotFoundException()));
|
||||||
|
|
||||||
assertThrows(ExtensionNotFoundException.class, () -> getHandler.handle(serverRequest));
|
Mono<ServerResponse> responseMono = getHandler.handle(serverRequest);
|
||||||
|
StepVerifier.create(responseMono)
|
||||||
|
.expectError(ExtensionNotFoundException.class)
|
||||||
|
.verify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,17 +16,18 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.ListResult;
|
import run.halo.app.extension.ListResult;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ExtensionListHandlerTest {
|
class ExtensionListHandlerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildPathPatternCorrectly() {
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
@ -44,7 +45,7 @@ class ExtensionListHandlerTest {
|
||||||
final var fake = new FakeExtension();
|
final var fake = new FakeExtension();
|
||||||
var fakeListResult = new ListResult<>(0, 0, 1, List.of(fake));
|
var fakeListResult = new ListResult<>(0, 0, 1, List.of(fake));
|
||||||
when(client.list(same(FakeExtension.class), any(), any(), anyInt(), anyInt()))
|
when(client.list(same(FakeExtension.class), any(), any(), anyInt(), anyInt()))
|
||||||
.thenReturn(fakeListResult);
|
.thenReturn(Mono.just(fakeListResult));
|
||||||
|
|
||||||
var responseMono = listHandler.handle(serverRequest);
|
var responseMono = listHandler.handle(serverRequest);
|
||||||
|
|
||||||
|
|
|
@ -15,10 +15,9 @@ import org.springframework.web.reactive.function.server.HandlerStrategies;
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.router.ExtensionRouterFunctionFactory;
|
|
||||||
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.CreateHandler;
|
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.CreateHandler;
|
||||||
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.GetHandler;
|
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.GetHandler;
|
||||||
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.ListHandler;
|
import run.halo.app.extension.router.ExtensionRouterFunctionFactory.ListHandler;
|
||||||
|
@ -28,7 +27,7 @@ import run.halo.app.extension.router.ExtensionRouterFunctionFactory.UpdateHandle
|
||||||
class ExtensionRouterFunctionFactoryTest {
|
class ExtensionRouterFunctionFactoryTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldCreateSuccessfully() {
|
void shouldCreateSuccessfully() {
|
||||||
|
|
|
@ -14,7 +14,6 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
@ -23,21 +22,21 @@ import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
|
||||||
import org.springframework.web.reactive.function.server.EntityResponse;
|
import org.springframework.web.reactive.function.server.EntityResponse;
|
||||||
|
import org.springframework.web.server.ServerWebInputException;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.FakeExtension;
|
import run.halo.app.extension.FakeExtension;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Scheme;
|
import run.halo.app.extension.Scheme;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.extension.exception.ExtensionConvertException;
|
|
||||||
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ExtensionUpdateHandlerTest {
|
class ExtensionUpdateHandlerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldBuildPathPatternCorrectly() {
|
void shouldBuildPathPatternCorrectly() {
|
||||||
|
@ -62,7 +61,8 @@ class ExtensionUpdateHandlerTest {
|
||||||
var serverRequest = MockServerRequest.builder()
|
var serverRequest = MockServerRequest.builder()
|
||||||
.pathVariable("name", "my-fake")
|
.pathVariable("name", "my-fake")
|
||||||
.body(Mono.just(unstructured));
|
.body(Mono.just(unstructured));
|
||||||
when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Optional.of(fake));
|
// when(client.fetch(eq(FakeExtension.class), eq("my-fake"))).thenReturn(Mono.just(fake));
|
||||||
|
when(client.update(eq(unstructured))).thenReturn(Mono.just(unstructured));
|
||||||
|
|
||||||
var scheme = Scheme.buildFromType(FakeExtension.class);
|
var scheme = Scheme.buildFromType(FakeExtension.class);
|
||||||
var updateHandler = new ExtensionUpdateHandler(scheme, client);
|
var updateHandler = new ExtensionUpdateHandler(scheme, client);
|
||||||
|
@ -73,10 +73,10 @@ class ExtensionUpdateHandlerTest {
|
||||||
assertEquals(HttpStatus.OK, response.statusCode());
|
assertEquals(HttpStatus.OK, response.statusCode());
|
||||||
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
assertEquals(MediaType.APPLICATION_JSON, response.headers().getContentType());
|
||||||
assertTrue(response instanceof EntityResponse<?>);
|
assertTrue(response instanceof EntityResponse<?>);
|
||||||
assertEquals(fake, ((EntityResponse<?>) response).entity());
|
assertEquals(unstructured, ((EntityResponse<?>) response).entity());
|
||||||
})
|
})
|
||||||
.verifyComplete();
|
.verifyComplete();
|
||||||
verify(client, times(1)).fetch(eq(FakeExtension.class), eq("my-fake"));
|
// verify(client, times(1)).fetch(eq(FakeExtension.class), eq("my-fake"));
|
||||||
verify(client, times(1)).update(eq(unstructured));
|
verify(client, times(1)).update(eq(unstructured));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +89,7 @@ class ExtensionUpdateHandlerTest {
|
||||||
var updateHandler = new ExtensionUpdateHandler(scheme, client);
|
var updateHandler = new ExtensionUpdateHandler(scheme, client);
|
||||||
var responseMono = updateHandler.handle(serverRequest);
|
var responseMono = updateHandler.handle(serverRequest);
|
||||||
StepVerifier.create(responseMono)
|
StepVerifier.create(responseMono)
|
||||||
.verifyError(ExtensionConvertException.class);
|
.verifyError(ServerWebInputException.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -4,17 +4,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import jakarta.persistence.EntityNotFoundException;
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
class ExtensionStoreClientJPAImplTest {
|
class ExtensionStoreClientJPAImplTest {
|
||||||
|
@ -33,7 +34,7 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
);
|
);
|
||||||
|
|
||||||
when(repository.findAllByNameStartingWith("/registry/posts"))
|
when(repository.findAllByNameStartingWith("/registry/posts"))
|
||||||
.thenReturn(expectedExtensions);
|
.thenReturn(Flux.fromIterable(expectedExtensions));
|
||||||
|
|
||||||
var gotExtensions = client.listByNamePrefix("/registry/posts");
|
var gotExtensions = client.listByNamePrefix("/registry/posts");
|
||||||
assertEquals(expectedExtensions, gotExtensions);
|
assertEquals(expectedExtensions, gotExtensions);
|
||||||
|
@ -44,8 +45,8 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
var expectedExtension =
|
var expectedExtension =
|
||||||
new ExtensionStore("/registry/posts/hello-world", "this is post".getBytes(), 1L);
|
new ExtensionStore("/registry/posts/hello-world", "this is post".getBytes(), 1L);
|
||||||
|
|
||||||
when(repository.findById("/registry/posts/hello-halo")).thenReturn(
|
when(repository.findById("/registry/posts/hello-halo"))
|
||||||
Optional.of(expectedExtension));
|
.thenReturn(Mono.just(expectedExtension));
|
||||||
|
|
||||||
var gotExtension = client.fetchByName("/registry/posts/hello-halo");
|
var gotExtension = client.fetchByName("/registry/posts/hello-halo");
|
||||||
assertTrue(gotExtension.isPresent());
|
assertTrue(gotExtension.isPresent());
|
||||||
|
@ -57,7 +58,8 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
var expectedExtension =
|
var expectedExtension =
|
||||||
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
when(repository.save(any())).thenReturn(expectedExtension);
|
when(repository.save(any()))
|
||||||
|
.thenReturn(Mono.just(expectedExtension));
|
||||||
|
|
||||||
var createdExtension =
|
var createdExtension =
|
||||||
client.create("/registry/posts/hello-halo", "hello halo".getBytes());
|
client.create("/registry/posts/hello-halo", "hello halo".getBytes());
|
||||||
|
@ -70,7 +72,7 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
var expectedExtension =
|
var expectedExtension =
|
||||||
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
when(repository.save(any())).thenReturn(expectedExtension);
|
when(repository.save(any())).thenReturn(Mono.just(expectedExtension));
|
||||||
|
|
||||||
var updatedExtension =
|
var updatedExtension =
|
||||||
client.update("/registry/posts/hello-halo", 1L, "hello halo".getBytes());
|
client.update("/registry/posts/hello-halo", 1L, "hello halo".getBytes());
|
||||||
|
@ -81,7 +83,7 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
@Test
|
@Test
|
||||||
void shouldThrowEntityNotFoundExceptionWhenDeletingNonExistExt() {
|
void shouldThrowEntityNotFoundExceptionWhenDeletingNonExistExt() {
|
||||||
|
|
||||||
when(repository.findById(any())).thenReturn(Optional.empty());
|
when(repository.findById(anyString())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
assertThrows(EntityNotFoundException.class,
|
assertThrows(EntityNotFoundException.class,
|
||||||
() -> client.delete("/registry/posts/hello-halo", 1L));
|
() -> client.delete("/registry/posts/hello-halo", 1L));
|
||||||
|
@ -92,8 +94,8 @@ class ExtensionStoreClientJPAImplTest {
|
||||||
var expectedExtension =
|
var expectedExtension =
|
||||||
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
when(repository.findById(any())).thenReturn(Optional.of(expectedExtension));
|
when(repository.findById(anyString())).thenReturn(Mono.just(expectedExtension));
|
||||||
doNothing().when(repository).delete(any());
|
when(repository.delete(any())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
var deletedExtension = client.delete("/registry/posts/hello-halo", 2L);
|
var deletedExtension = client.delete("/registry/posts/hello-halo", 2L);
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,6 @@ import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
@ -25,8 +24,10 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.skyscreamer.jsonassert.JSONAssert;
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.util.FileSystemUtils;
|
import org.springframework.util.FileSystemUtils;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.extension.GroupVersionKind;
|
import run.halo.app.extension.GroupVersionKind;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.extension.Unstructured;
|
import run.halo.app.extension.Unstructured;
|
||||||
import run.halo.app.infra.properties.HaloProperties;
|
import run.halo.app.infra.properties.HaloProperties;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
@ -41,7 +42,7 @@ import run.halo.app.infra.utils.JsonUtils;
|
||||||
class ExtensionResourceInitializerTest {
|
class ExtensionResourceInitializerTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExtensionClient extensionClient;
|
private ReactiveExtensionClient extensionClient;
|
||||||
@Mock
|
@Mock
|
||||||
private HaloProperties haloProperties;
|
private HaloProperties haloProperties;
|
||||||
@Mock
|
@Mock
|
||||||
|
@ -119,12 +120,16 @@ class ExtensionResourceInitializerTest {
|
||||||
@Test
|
@Test
|
||||||
void onApplicationEvent() throws JSONException {
|
void onApplicationEvent() throws JSONException {
|
||||||
when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true);
|
when(haloProperties.isRequiredExtensionDisabled()).thenReturn(true);
|
||||||
ArgumentCaptor<Unstructured> argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);
|
var argumentCaptor = ArgumentCaptor.forClass(Unstructured.class);
|
||||||
|
|
||||||
when(extensionClient.fetch(any(GroupVersionKind.class), any()))
|
when(extensionClient.fetch(any(GroupVersionKind.class), any()))
|
||||||
.thenReturn(Optional.empty());
|
.thenReturn(Mono.empty());
|
||||||
|
when(extensionClient.create(any())).thenReturn(Mono.empty());
|
||||||
|
|
||||||
|
var initializeMono = extensionResourceInitializer.initialize(applicationReadyEvent);
|
||||||
|
StepVerifier.create(initializeMono)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
extensionResourceInitializer.onApplicationEvent(applicationReadyEvent);
|
|
||||||
|
|
||||||
verify(extensionClient, times(3)).create(argumentCaptor.capture());
|
verify(extensionClient, times(3)).create(argumentCaptor.capture());
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package run.halo.app.plugin;
|
package run.halo.app.plugin;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ -62,17 +63,20 @@ class PluginCompositeRouterFunctionTest {
|
||||||
handlerFunction = request -> ServerResponse.ok().build();
|
handlerFunction = request -> ServerResponse.ok().build();
|
||||||
routerFunction = request -> Mono.just(handlerFunction);
|
routerFunction = request -> Mono.just(handlerFunction);
|
||||||
|
|
||||||
ObjectProvider objectProvider = mock(ObjectProvider.class);
|
var objectProvider = mock(ObjectProvider.class);
|
||||||
when(objectProvider.orderedStream()).thenReturn(Stream.of(routerFunction));
|
when(objectProvider.orderedStream()).thenReturn(Stream.of(routerFunction));
|
||||||
|
|
||||||
when(pluginApplicationContext.getBeanProvider(RouterFunction.class))
|
when(pluginApplicationContext.getBeanProvider(RouterFunction.class))
|
||||||
.thenReturn(objectProvider);
|
.thenReturn(objectProvider);
|
||||||
|
when(reverseProxyRouterFunctionFactory.create(any())).thenReturn(Mono.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void route() {
|
void route() {
|
||||||
// trigger haloPluginStartedEvent
|
// trigger haloPluginStartedEvent
|
||||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
||||||
|
new HaloPluginStartedEvent(this, pluginWrapper)))
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
RouterFunctionMapping mapping = new RouterFunctionMapping(compositeRouterFunction);
|
||||||
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
mapping.setMessageReaders(this.codecConfigurer.getReaders());
|
||||||
|
@ -90,14 +94,18 @@ class PluginCompositeRouterFunctionTest {
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isNull();
|
||||||
|
|
||||||
// trigger haloPluginStartedEvent
|
// trigger haloPluginStartedEvent
|
||||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
||||||
|
new HaloPluginStartedEvent(this, pluginWrapper)))
|
||||||
|
.verifyComplete();
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void onPluginStopped() {
|
void onPluginStopped() {
|
||||||
// trigger haloPluginStartedEvent
|
// trigger haloPluginStartedEvent
|
||||||
compositeRouterFunction.onPluginStarted(new HaloPluginStartedEvent(this, pluginWrapper));
|
StepVerifier.create(compositeRouterFunction.onPluginStarted(
|
||||||
|
new HaloPluginStartedEvent(this, pluginWrapper)))
|
||||||
|
.verifyComplete();
|
||||||
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
assertThat(compositeRouterFunction.getRouterFunction("fakeA")).isEqualTo(routerFunction);
|
||||||
|
|
||||||
// trigger HaloPluginStoppedEvent
|
// trigger HaloPluginStoppedEvent
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package run.halo.app.plugin.resources;
|
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.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
@ -12,11 +11,11 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import reactor.core.publisher.Flux;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import reactor.test.StepVerifier;
|
||||||
import run.halo.app.core.extension.ReverseProxy;
|
import run.halo.app.core.extension.ReverseProxy;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
import run.halo.app.plugin.HaloPluginManager;
|
import run.halo.app.plugin.HaloPluginManager;
|
||||||
import run.halo.app.plugin.PluginApplicationContext;
|
import run.halo.app.plugin.PluginApplicationContext;
|
||||||
import run.halo.app.plugin.PluginConst;
|
import run.halo.app.plugin.PluginConst;
|
||||||
|
@ -31,7 +30,7 @@ import run.halo.app.plugin.PluginConst;
|
||||||
class ReverseProxyRouterFunctionFactoryTest {
|
class ReverseProxyRouterFunctionFactoryTest {
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private ExtensionClient extensionClient;
|
private ReactiveExtensionClient extensionClient;
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private PluginApplicationContext pluginApplicationContext;
|
private PluginApplicationContext pluginApplicationContext;
|
||||||
|
@ -50,14 +49,15 @@ class ReverseProxyRouterFunctionFactoryTest {
|
||||||
|
|
||||||
when(pluginApplicationContext.getPluginId()).thenReturn("fakeA");
|
when(pluginApplicationContext.getPluginId()).thenReturn("fakeA");
|
||||||
when(extensionClient.list(eq(ReverseProxy.class), any(), any())).thenReturn(
|
when(extensionClient.list(eq(ReverseProxy.class), any(), any())).thenReturn(
|
||||||
List.of(reverseProxy));
|
Flux.just(reverseProxy));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void create() {
|
void create() {
|
||||||
RouterFunction<ServerResponse> routerFunction =
|
var routerFunction = reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
||||||
reverseProxyRouterFunctionFactory.create(pluginApplicationContext);
|
StepVerifier.create(routerFunction)
|
||||||
assertThat(routerFunction).isNotNull();
|
.expectNextCount(1)
|
||||||
|
.verifyComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -43,8 +43,10 @@ class DefaultUserDetailServiceTest {
|
||||||
void shouldUpdatePasswordSuccessfully() {
|
void shouldUpdatePasswordSuccessfully() {
|
||||||
var fakeUser = createFakeUserDetails();
|
var fakeUser = createFakeUserDetails();
|
||||||
|
|
||||||
|
var user = new run.halo.app.core.extension.User();
|
||||||
|
|
||||||
when(userService.updatePassword("faker", "new-fake-password")).thenReturn(
|
when(userService.updatePassword("faker", "new-fake-password")).thenReturn(
|
||||||
Mono.just("").then()
|
Mono.just(user)
|
||||||
);
|
);
|
||||||
|
|
||||||
var userDetailsMono = userDetailService.updatePassword(fakeUser, "new-fake-password");
|
var userDetailsMono = userDetailService.updatePassword(fakeUser, "new-fake-password");
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.RoleBinding;
|
import run.halo.app.core.extension.RoleBinding;
|
||||||
import run.halo.app.core.extension.User;
|
import run.halo.app.core.extension.User;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ReactiveExtensionClient;
|
||||||
|
|
||||||
@SpringBootTest(properties = {"halo.security.initializer.disabled=false",
|
@SpringBootTest(properties = {"halo.security.initializer.disabled=false",
|
||||||
"halo.security.initializer.super-admin-username=fake-admin",
|
"halo.security.initializer.super-admin-username=fake-admin",
|
||||||
|
@ -24,9 +24,8 @@ import run.halo.app.extension.ExtensionClient;
|
||||||
@AutoConfigureTestDatabase
|
@AutoConfigureTestDatabase
|
||||||
class SuperAdminInitializerTest {
|
class SuperAdminInitializerTest {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
@SpyBean
|
@SpyBean
|
||||||
ExtensionClient client;
|
ReactiveExtensionClient client;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
WebTestClient webClient;
|
WebTestClient webClient;
|
||||||
|
|
|
@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.mockito.ArgumentMatchers.anyString;
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
|
@ -14,6 +15,9 @@ import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
|
||||||
import org.springframework.security.core.userdetails.User;
|
import org.springframework.security.core.userdetails.User;
|
||||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
import run.halo.app.core.extension.Role;
|
||||||
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
import run.halo.app.security.LoginUtils;
|
import run.halo.app.security.LoginUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -26,6 +30,9 @@ class JwtAuthenticationTest {
|
||||||
@MockBean
|
@MockBean
|
||||||
ReactiveUserDetailsService userDetailsService;
|
ReactiveUserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
RoleService roleService;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void accessProtectedApiWithoutToken() {
|
void accessProtectedApiWithoutToken() {
|
||||||
webClient.get().uri("/api/v1/test/hello").exchange().expectStatus().isUnauthorized();
|
webClient.get().uri("/api/v1/test/hello").exchange().expectStatus().isUnauthorized();
|
||||||
|
@ -40,6 +47,19 @@ class JwtAuthenticationTest {
|
||||||
.roles("USER")
|
.roles("USER")
|
||||||
.build()
|
.build()
|
||||||
));
|
));
|
||||||
|
|
||||||
|
var role = new Role();
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("USER");
|
||||||
|
role.setMetadata(metadata);
|
||||||
|
role.setRules(List.of(new Role.PolicyRule.Builder()
|
||||||
|
.apiGroups("")
|
||||||
|
.resources("test")
|
||||||
|
.resourceNames("hello")
|
||||||
|
.build()));
|
||||||
|
when(roleService.getMonoRole("authenticated")).thenReturn(Mono.empty());
|
||||||
|
when(roleService.getMonoRole("USER")).thenReturn(Mono.just(role));
|
||||||
|
|
||||||
final var token = LoginUtils.login(webClient, "username", "password").block();
|
final var token = LoginUtils.login(webClient, "username", "password").block();
|
||||||
webClient.get().uri("/api/v1/test/hello")
|
webClient.get().uri("/api/v1/test/hello")
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package run.halo.app.security.authorization;
|
package run.halo.app.security.authorization;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.times;
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
@ -31,6 +32,7 @@ import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.core.extension.Role;
|
import run.halo.app.core.extension.Role;
|
||||||
import run.halo.app.core.extension.Role.PolicyRule;
|
import run.halo.app.core.extension.Role.PolicyRule;
|
||||||
import run.halo.app.core.extension.service.RoleService;
|
import run.halo.app.core.extension.service.RoleService;
|
||||||
|
import run.halo.app.extension.exception.ExtensionNotFoundException;
|
||||||
import run.halo.app.security.LoginUtils;
|
import run.halo.app.security.LoginUtils;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@ -51,12 +53,15 @@ class AuthorizationTest {
|
||||||
void accessProtectedApiWithoutSufficientRole() {
|
void accessProtectedApiWithoutSufficientRole() {
|
||||||
when(userDetailsService.findByUsername(eq("user"))).thenReturn(
|
when(userDetailsService.findByUsername(eq("user"))).thenReturn(
|
||||||
Mono.just(User.withDefaultPasswordEncoder().username("user").password("password")
|
Mono.just(User.withDefaultPasswordEncoder().username("user").password("password")
|
||||||
// .roles("role-template-view-posts", "role-template-manage-posts")
|
|
||||||
.roles("invalid-role").build()));
|
.roles("invalid-role").build()));
|
||||||
|
when(roleService.getMonoRole(any())).thenReturn(Mono.empty());
|
||||||
var token = LoginUtils.login(webClient, "user", "password").block();
|
var token = LoginUtils.login(webClient, "user", "password").block();
|
||||||
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange().expectStatus()
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange().expectStatus()
|
||||||
.isForbidden();
|
.isForbidden();
|
||||||
|
|
||||||
|
verify(roleService, times(1)).getMonoRole("authenticated");
|
||||||
|
verify(roleService, times(1)).getMonoRole("invalid-role");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -70,7 +75,9 @@ class AuthorizationTest {
|
||||||
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
|
new PolicyRule.Builder().apiGroups("fake.halo.run").verbs("list").resources("posts")
|
||||||
.build()));
|
.build()));
|
||||||
|
|
||||||
when(roleService.getRole(eq("post.read"))).thenReturn(role);
|
when(roleService.getMonoRole("post.read")).thenReturn(Mono.just(role));
|
||||||
|
when(roleService.getMonoRole("authenticated")).thenReturn(
|
||||||
|
Mono.error(ExtensionNotFoundException::new));
|
||||||
|
|
||||||
var token = LoginUtils.login(webClient, "user", "password").block();
|
var token = LoginUtils.login(webClient, "user", "password").block();
|
||||||
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
webClient.get().uri("/apis/fake.halo.run/v1/posts")
|
||||||
|
@ -83,7 +90,8 @@ class AuthorizationTest {
|
||||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
|
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).exchange()
|
||||||
.expectStatus().isForbidden();
|
.expectStatus().isForbidden();
|
||||||
|
|
||||||
verify(roleService, times(2)).getRole("post.read");
|
verify(roleService, times(2)).getMonoRole("authenticated");
|
||||||
|
verify(roleService, times(2)).getMonoRole("post.read");
|
||||||
}
|
}
|
||||||
|
|
||||||
@TestConfiguration
|
@TestConfiguration
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package run.halo.app.theme.dialect;
|
package run.halo.app.theme.dialect;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
import static run.halo.app.theme.ThemeLocaleContextResolver.TIME_ZONE_COOKIE_NAME;
|
import static run.halo.app.theme.ThemeLocaleContextResolver.TIME_ZONE_COOKIE_NAME;
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
@ -19,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.context.TestConfiguration;
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
@ -29,6 +32,7 @@ import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
import org.thymeleaf.extras.java8time.expression.Temporals;
|
import org.thymeleaf.extras.java8time.expression.Temporals;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.theme.ThemeContext;
|
import run.halo.app.theme.ThemeContext;
|
||||||
import run.halo.app.theme.ThemeResolver;
|
import run.halo.app.theme.ThemeResolver;
|
||||||
|
|
||||||
|
@ -43,7 +47,8 @@ import run.halo.app.theme.ThemeResolver;
|
||||||
class ThemeJava8TimeDialectIntegrationTest {
|
class ThemeJava8TimeDialectIntegrationTest {
|
||||||
private static final Instant INSTANT = Instant.now();
|
private static final Instant INSTANT = Instant.now();
|
||||||
|
|
||||||
@Autowired
|
// @Autowired
|
||||||
|
@SpyBean
|
||||||
private ThemeResolver themeResolver;
|
private ThemeResolver themeResolver;
|
||||||
|
|
||||||
private URL defaultThemeUrl;
|
private URL defaultThemeUrl;
|
||||||
|
@ -57,17 +62,15 @@ class ThemeJava8TimeDialectIntegrationTest {
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws FileNotFoundException {
|
void setUp() throws FileNotFoundException {
|
||||||
themeContextFunction = themeResolver.getThemeContextFunction();
|
|
||||||
themeResolver.setThemeContextFunction(request -> createDefaultContext());
|
|
||||||
|
|
||||||
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
||||||
|
when(themeResolver.getTheme(any(ServerHttpRequest.class))).thenReturn(
|
||||||
|
Mono.just(createDefaultContext()));
|
||||||
defaultTimeZone = TimeZone.getDefault();
|
defaultTimeZone = TimeZone.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
void tearDown() {
|
void tearDown() {
|
||||||
TimeZone.setDefault(defaultTimeZone);
|
TimeZone.setDefault(defaultTimeZone);
|
||||||
this.themeResolver.setThemeContextFunction(themeContextFunction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -3,14 +3,14 @@ package run.halo.app.theme.message;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.function.Function;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.mockito.Mockito;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
import org.springframework.boot.test.context.TestConfiguration;
|
import org.springframework.boot.test.context.TestConfiguration;
|
||||||
|
import org.springframework.boot.test.mock.mockito.SpyBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
@ -20,6 +20,7 @@ import org.springframework.web.reactive.function.server.RequestPredicates;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||||
import org.springframework.web.reactive.function.server.RouterFunctions;
|
import org.springframework.web.reactive.function.server.RouterFunctions;
|
||||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
import run.halo.app.theme.ThemeContext;
|
import run.halo.app.theme.ThemeContext;
|
||||||
import run.halo.app.theme.ThemeResolver;
|
import run.halo.app.theme.ThemeResolver;
|
||||||
|
|
||||||
|
@ -33,34 +34,27 @@ import run.halo.app.theme.ThemeResolver;
|
||||||
@AutoConfigureWebTestClient
|
@AutoConfigureWebTestClient
|
||||||
public class ThemeMessageResolverIntegrationTest {
|
public class ThemeMessageResolverIntegrationTest {
|
||||||
|
|
||||||
@Autowired
|
@SpyBean
|
||||||
private ThemeResolver themeResolver;
|
private ThemeResolver themeResolver;
|
||||||
|
|
||||||
private URL defaultThemeUrl;
|
private URL defaultThemeUrl;
|
||||||
|
|
||||||
private URL otherThemeUrl;
|
private URL otherThemeUrl;
|
||||||
|
|
||||||
Function<ServerHttpRequest, ThemeContext> themeContextFunction;
|
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private WebTestClient webTestClient;
|
private WebTestClient webTestClient;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws FileNotFoundException {
|
void setUp() throws FileNotFoundException {
|
||||||
themeContextFunction = themeResolver.getThemeContextFunction();
|
|
||||||
|
|
||||||
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
defaultThemeUrl = ResourceUtils.getURL("classpath:themes/default");
|
||||||
otherThemeUrl = ResourceUtils.getURL("classpath:themes/other");
|
otherThemeUrl = ResourceUtils.getURL("classpath:themes/other");
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
Mockito.when(themeResolver.getTheme(Mockito.any(ServerHttpRequest.class)))
|
||||||
void tearDown() {
|
.thenReturn(Mono.just(createDefaultContext()));
|
||||||
this.themeResolver.setThemeContextFunction(themeContextFunction);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void messageResolverWhenDefaultTheme() {
|
void messageResolverWhenDefaultTheme() {
|
||||||
themeResolver.setThemeContextFunction(request -> createDefaultContext());
|
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/?language=zh")
|
.uri("/?language=zh")
|
||||||
.exchange()
|
.exchange()
|
||||||
|
@ -85,7 +79,6 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void messageResolverForEnLanguageWhenDefaultTheme() {
|
void messageResolverForEnLanguageWhenDefaultTheme() {
|
||||||
themeResolver.setThemeContextFunction(request -> createDefaultContext());
|
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/?language=en")
|
.uri("/?language=en")
|
||||||
.exchange()
|
.exchange()
|
||||||
|
@ -110,7 +103,6 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldUseDefaultWhenLanguageNotSupport() {
|
void shouldUseDefaultWhenLanguageNotSupport() {
|
||||||
themeResolver.setThemeContextFunction(request -> createDefaultContext());
|
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/index?language=foo")
|
.uri("/index?language=foo")
|
||||||
.exchange()
|
.exchange()
|
||||||
|
@ -135,7 +127,6 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void switchTheme() {
|
void switchTheme() {
|
||||||
themeResolver.setThemeContextFunction(request -> createDefaultContext());
|
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/index?language=zh")
|
.uri("/index?language=zh")
|
||||||
.exchange()
|
.exchange()
|
||||||
|
@ -158,7 +149,8 @@ public class ThemeMessageResolverIntegrationTest {
|
||||||
""");
|
""");
|
||||||
|
|
||||||
// For other theme
|
// For other theme
|
||||||
themeResolver.setThemeContextFunction(request -> createOtherContext());
|
Mockito.when(themeResolver.getTheme(Mockito.any(ServerHttpRequest.class)))
|
||||||
|
.thenReturn(Mono.just(createOtherContext()));
|
||||||
webTestClient.get()
|
webTestClient.get()
|
||||||
.uri("/index?language=zh")
|
.uri("/index?language=zh")
|
||||||
.exchange()
|
.exchange()
|
||||||
|
|
|
@ -4,18 +4,13 @@ spring:
|
||||||
output:
|
output:
|
||||||
ansi:
|
ansi:
|
||||||
enabled: detect
|
enabled: detect
|
||||||
datasource:
|
r2dbc:
|
||||||
type: com.zaxxer.hikari.HikariDataSource
|
name: halo-test
|
||||||
driver-class-name: org.h2.Driver
|
generate-unique-name: true
|
||||||
url: jdbc:h2:mem:halo
|
sql:
|
||||||
username: admin
|
init:
|
||||||
password: 123456
|
mode: always
|
||||||
|
platform: h2
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: update
|
|
||||||
open-in-view: false
|
|
||||||
show-sql: true
|
|
||||||
|
|
||||||
halo:
|
halo:
|
||||||
work-dir: ${user.home}/halo-next-test
|
work-dir: ${user.home}/halo-next-test
|
||||||
|
@ -36,3 +31,4 @@ springdoc:
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
run.halo.app: debug
|
run.halo.app: debug
|
||||||
|
org.springframework.r2dbc: DEBUG
|
||||||
|
|
Loading…
Reference in New Issue