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
John Niang 2022-08-17 10:56:11 +08:00 committed by GitHub
parent 84eef54603
commit 9911ba927d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
71 changed files with 1514 additions and 726 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
/** /**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,7 @@
create table if not exists extensions
(
name varchar(255) not null,
data blob,
version bigint,
primary key (name)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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