feat: support validate requires version for plugin (#3114)

#### What type of PR is this?
/kind feature
/area core
/milestone 2.2.x

#### What this PR does / why we need it:
插件安装和升级支持版本校验

BTW: 此 PR 中 PluginReconciler 有一些异常提示是没有加 i18n 的,主要是考虑 Reconciler 与请求不挂钩,无法获取到 request 上下文的 Locale,如果用 Locale.getDefault() 那么后续用户切换语言时也更改不到已经持久化到数据库中的错误信息,可能得靠客户端翻译异常。

参考文档:
- [semver-expressions-api-ranges](https://github.com/zafarkhaja/jsemver#semver-expressions-api-ranges)
- [integrating-with-actuator.build-info](https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#integrating-with-actuator.build-info)
- [BuildInfoContributor](https://github.com/spring-projects/spring-boot/blob/v3.0.1/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/info/BuildInfoContributor.java)

#### Which issue(s) this PR fixes:
Fixes #3089

#### Special notes for your reviewer:
how to test it?
- 开发模式下不会校验插件填写的 requires,但通过接口安装和升级都会统一校验。
- 在 deployment 模式下安装插件和升级插件会根据 halo 的版本校验插件的 spec.requires 是否符合要求,参考 [semver-expressions-api-ranges](https://github.com/zafarkhaja/jsemver#semver-expressions-api-ranges)。
- 如果 spec.requires 为 `*` 则表示允许所有,如果填写为具体的版本号,例如 requires: "2.2.0" 则隐式表示为 `>=2.2.0`。

可以测试这几种情况是否符合期望。

/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?
```release-note
插件安装和升级支持版本校验
```
pull/3148/head^2
guqing 2023-01-13 10:50:12 +08:00 committed by GitHub
parent 2a70d59350
commit 5c29ab5750
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 358 additions and 27 deletions

View File

@ -27,6 +27,10 @@ configurations {
}
}
springBoot {
buildInfo()
}
bootJar {
manifest {
attributes "Implementation-Title": "Halo Application",

View File

@ -14,6 +14,7 @@ import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersF
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
import static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;
import com.github.zafarkhaja.semver.Version;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Schema;
@ -29,6 +30,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Predicate;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
import org.springframework.dao.OptimisticLockingFailureException;
@ -38,6 +40,7 @@ import org.springframework.http.codec.multipart.FilePart;
import org.springframework.http.codec.multipart.Part;
import org.springframework.retry.RetryException;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.function.server.RouterFunction;
@ -57,22 +60,23 @@ import run.halo.app.extension.Comparators;
import run.halo.app.extension.ConfigMap;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.extension.router.IListRequest.QueryListRequest;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.exception.UnsatisfiedAttributeValueException;
import run.halo.app.infra.utils.FileUtils;
import run.halo.app.infra.utils.VersionUtils;
import run.halo.app.plugin.PluginProperties;
import run.halo.app.plugin.YamlPluginFinder;
@Slf4j
@Component
@AllArgsConstructor
public class PluginEndpoint implements CustomEndpoint {
private final PluginProperties pluginProperties;
private final ReactiveExtensionClient client;
public PluginEndpoint(PluginProperties pluginProperties, ReactiveExtensionClient client) {
this.pluginProperties = pluginProperties;
this.client = client;
}
private final SystemVersionSupplier systemVersionSupplier;
@Override
public RouterFunction<ServerResponse> endpoint() {
@ -172,6 +176,8 @@ public class PluginEndpoint implements CustomEndpoint {
throw new ServerWebInputException(
"The uploaded plugin doesn't match the given plugin name");
}
satisfiesRequiresVersion(newPlugin);
})
.flatMap(newPlugin -> deletePluginAndWaitForComplete(newPlugin.getMetadata().getName())
.map(oldPlugin -> {
@ -187,7 +193,6 @@ public class PluginEndpoint implements CustomEndpoint {
var pluginRoot = Paths.get(pluginProperties.getPluginsRoot());
createDirectoriesIfNotExists(pluginRoot);
var tempPluginPath = tempPluginPathRef.get();
var filename = tempPluginPath.getFileName().toString();
copy(tempPluginPath, pluginRoot.resolve(newPlugin.generateFileName()));
} catch (IOException e) {
throw Exceptions.propagate(e);
@ -198,6 +203,23 @@ public class PluginEndpoint implements CustomEndpoint {
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDirRef.get()));
}
private void satisfiesRequiresVersion(Plugin newPlugin) {
Assert.notNull(newPlugin, "The plugin must not be null.");
Version version = systemVersionSupplier.get();
// validate the plugin version
// only use the nominal system version to compare, the format is like MAJOR.MINOR.PATCH
String systemVersion = version.getNormalVersion();
String requires = newPlugin.getSpec().getRequires();
if (!VersionUtils.satisfiesRequires(systemVersion, requires)) {
throw new UnsatisfiedAttributeValueException(String.format(
"Plugin requires a minimum system version of [%s], but the current version is "
+ "[%s].",
requires, systemVersion),
"problemDetail.plugin.version.unsatisfied.requires",
new String[] {requires, systemVersion});
}
}
private Mono<Plugin> deletePluginAndWaitForComplete(String pluginName) {
return client.fetch(Plugin.class, pluginName)
.flatMap(client::delete)
@ -332,6 +354,8 @@ public class PluginEndpoint implements CustomEndpoint {
.flatMap(this::transferToTemp)
.flatMap(tempJarFilePath -> {
var plugin = new YamlPluginFinder().find(tempJarFilePath);
// validate the plugin version
satisfiesRequiresVersion(plugin);
// Disable auto enable during installation
plugin.getSpec().setEnabled(false);
return client.fetch(Plugin.class, plugin.getMetadata().getName())

View File

@ -11,6 +11,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.PluginRuntimeException;
@ -41,17 +42,12 @@ import run.halo.app.plugin.resources.BundleResourceUtils;
*/
@Slf4j
@Component
@AllArgsConstructor
public class PluginReconciler implements Reconciler<Request> {
private static final String FINALIZER_NAME = "plugin-protection";
private final ExtensionClient client;
private final HaloPluginManager haloPluginManager;
public PluginReconciler(ExtensionClient client,
HaloPluginManager haloPluginManager) {
this.client = client;
this.haloPluginManager = haloPluginManager;
}
@Override
public Result reconcile(Request request) {
client.fetch(Plugin.class, request.name())
@ -138,6 +134,12 @@ public class PluginReconciler implements Reconciler<Request> {
private void startPlugin(String pluginName) {
client.fetch(Plugin.class, pluginName).ifPresent(plugin -> {
final Plugin oldPlugin = JsonUtils.deepCopy(plugin);
// verify plugin meets the preconditions for startup
if (!verifyStartCondition(pluginName)) {
return;
}
if (shouldReconcileStartState(plugin)) {
PluginState currentState = haloPluginManager.startPlugin(pluginName);
handleStatus(plugin, currentState, PluginState.STARTED);
@ -159,6 +161,47 @@ public class PluginReconciler implements Reconciler<Request> {
});
}
private boolean verifyStartCondition(String pluginName) {
PluginWrapper pluginWrapper = haloPluginManager.getPlugin(pluginName);
return client.fetch(Plugin.class, pluginName).map(plugin -> {
Plugin.PluginStatus oldStatus = JsonUtils.deepCopy(plugin.statusNonNull());
Plugin.PluginStatus status = plugin.statusNonNull();
status.setLastTransitionTime(Instant.now());
if (pluginWrapper == null) {
status.setPhase(PluginState.FAILED);
status.setReason("PluginNotFound");
status.setMessage("Plugin [" + pluginName + "] not found in plugin manager");
if (!oldStatus.equals(status)) {
client.update(plugin);
}
return false;
}
// Check if this plugin version is match requires param.
if (!haloPluginManager.validatePluginVersion(pluginWrapper)) {
status.setPhase(PluginState.FAILED);
status.setReason("PluginVersionNotMatch");
String message = String.format(
"Plugin requires a minimum system version of [%s], and you have [%s].",
plugin.getSpec().getRequires(), haloPluginManager.getSystemVersion());
status.setMessage(message);
if (!oldStatus.equals(status)) {
client.update(plugin);
}
return false;
}
PluginState pluginState = pluginWrapper.getPluginState();
if (PluginState.DISABLED.equals(pluginState)) {
status.setPhase(pluginState);
status.setReason("PluginDisabled");
status.setMessage("The plugin is disabled for some reason and cannot be started.");
}
return true;
}).orElse(false);
}
private boolean shouldReconcileStopState(Plugin plugin) {
return !plugin.getSpec().getEnabled()
&& plugin.statusNonNull().getPhase() == PluginState.STARTED;
@ -187,11 +230,15 @@ public class PluginReconciler implements Reconciler<Request> {
if (desiredState.equals(currentState)) {
plugin.getSpec().setEnabled(PluginState.STARTED.equals(currentState));
} else {
String pluginName = plugin.getMetadata().getName();
PluginStartingError startingError =
haloPluginManager.getPluginStartingError(plugin.getMetadata().getName());
if (startingError == null) {
startingError = PluginStartingError.of(pluginName, "Unknown error", "");
}
status.setReason(startingError.getMessage());
status.setMessage(startingError.getDevMessage());
// requeue the plugin for reconciliation
client.fetch(Plugin.class, pluginName).ifPresent(client::update);
throw new PluginRuntimeException(startingError.getMessage());
}
}

View File

@ -0,0 +1,37 @@
package run.halo.app.infra;
import com.github.zafarkhaja.semver.Version;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.info.BuildProperties;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
/**
* Default implementation of system version supplier.
*
* @author guqing
* @since 2.0.0
*/
@Component
public class DefaultSystemVersionSupplier implements SystemVersionSupplier {
private static final String DEFAULT_VERSION = "0.0.0";
@Nullable
private BuildProperties buildProperties;
@Autowired(required = false)
public void setBuildProperties(@Nullable BuildProperties buildProperties) {
this.buildProperties = buildProperties;
}
@Override
public Version get() {
if (buildProperties == null) {
return Version.valueOf(DEFAULT_VERSION);
}
String projectVersion =
StringUtils.defaultString(buildProperties.getVersion(), DEFAULT_VERSION);
return Version.valueOf(projectVersion);
}
}

View File

@ -0,0 +1,15 @@
package run.halo.app.infra;
import com.github.zafarkhaja.semver.Version;
import java.util.function.Supplier;
/**
* The supplier to gets the project version.
* If it cannot be obtained, return 0.0.0.
*
* @author guqing
* @see <a href="https://semver.org">Semantic Versioning 2.0.0</a>
* @since 2.0.0
*/
public interface SystemVersionSupplier extends Supplier<Version> {
}

View File

@ -0,0 +1,20 @@
package run.halo.app.infra.exception;
import jakarta.validation.constraints.Null;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ServerWebInputException;
/**
* {@link ServerWebInputException} subclass that indicates an unsatisfied
* attribute value in request parameters.
*
* @author guqing
* @since 2.2.0
*/
public class UnsatisfiedAttributeValueException extends ServerWebInputException {
public UnsatisfiedAttributeValueException(String reason, @Nullable String messageDetailCode,
@Null Object[] messageDetailArguments) {
super(reason, null, null, messageDetailCode, messageDetailArguments);
}
}

View File

@ -0,0 +1,49 @@
package run.halo.app.infra.utils;
import com.github.zafarkhaja.semver.Version;
import com.github.zafarkhaja.semver.expr.Expression;
import lombok.experimental.UtilityClass;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.server.ServerWebInputException;
@UtilityClass
public class VersionUtils {
/**
* Check if this "requires" param satisfies for a given (system) version.
*
* @param version the version to check
* @return true if version satisfies the "requires" or if requires was left blank
*/
public static boolean satisfiesRequires(String version, String requires) {
String requiresVersion = StringUtils.trim(requires);
// an exact version x.y.z will implicitly mean the same as >=x.y.z
if (requiresVersion.matches("^\\d+\\.\\d+\\.\\d+$")) {
// If exact versions are not allowed in requires, rewrite to >= expression
requiresVersion = ">=" + requiresVersion;
}
return version.equals("0.0.0") || checkVersionConstraint(version, requiresVersion);
}
/**
* Checks if a version satisfies the specified SemVer {@link Expression} string.
* If the constraint is empty or null then the method returns true.
* Constraint examples: {@code >2.0.0} (simple), {@code ">=1.4.0 & <1.6.0"} (range).
* See
* <a href="https://github.com/zafarkhaja/jsemver#semver-expressions-api-ranges">semver-expressions-api-ranges</a> for more info.
*
* @param version the version to check
* @param constraint the SemVer Expression string
* @return true if version satisfies the constraint or if constraint was left blank
*/
public static boolean checkVersionConstraint(String version, String constraint) {
try {
return StringUtils.isBlank(constraint)
|| "*".equals(constraint)
|| Version.valueOf(version).satisfies(constraint);
} catch (Exception e) {
throw new ServerWebInputException("Illegal requires version expression.", null, e);
}
}
}

View File

@ -25,6 +25,7 @@ import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import run.halo.app.plugin.event.HaloPluginBeforeStopEvent;
import run.halo.app.plugin.event.HaloPluginLoadedEvent;
import run.halo.app.plugin.event.HaloPluginStartedEvent;
@ -216,6 +217,11 @@ public class HaloPluginManager extends DefaultPluginManager
doStopPlugins();
}
public boolean validatePluginVersion(PluginWrapper pluginWrapper) {
Assert.notNull(pluginWrapper, "The pluginWrapper must not be null.");
return isPluginValid(pluginWrapper);
}
private PluginState doStartPlugin(String pluginId) {
checkPluginId(pluginId);

View File

@ -8,6 +8,7 @@ import java.lang.reflect.Constructor;
import java.nio.file.Path;
import java.time.Instant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.pf4j.ClassLoadingStrategy;
import org.pf4j.CompoundPluginLoader;
import org.pf4j.CompoundPluginRepository;
@ -27,13 +28,13 @@ import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.infra.SystemVersionSupplier;
/**
* Plugin autoconfiguration for Spring Boot.
@ -48,12 +49,16 @@ public class PluginAutoConfiguration {
private final PluginProperties pluginProperties;
private final SystemVersionSupplier systemVersionSupplier;
@Qualifier("webFluxContentTypeResolver")
private final RequestedContentTypeResolver requestedContentTypeResolver;
public PluginAutoConfiguration(PluginProperties pluginProperties,
SystemVersionSupplier systemVersionSupplier,
RequestedContentTypeResolver requestedContentTypeResolver) {
this.pluginProperties = pluginProperties;
this.systemVersionSupplier = systemVersionSupplier;
this.requestedContentTypeResolver = requestedContentTypeResolver;
}
@ -77,13 +82,12 @@ public class PluginAutoConfiguration {
// Setup Plugin folder
String pluginsRoot =
StringUtils.hasText(pluginProperties.getPluginsRoot())
? pluginProperties.getPluginsRoot()
: "plugins";
StringUtils.defaultString(pluginProperties.getPluginsRoot(), "plugins");
System.setProperty("pf4j.pluginsDir", pluginsRoot);
String appHome = System.getProperty("app.home");
if (RuntimeMode.DEPLOYMENT == pluginProperties.getRuntimeMode()
&& StringUtils.hasText(appHome)) {
&& StringUtils.isNotBlank(appHome)) {
System.setProperty("pf4j.pluginsDir", appHome + File.separator + pluginsRoot);
}
@ -170,10 +174,17 @@ public class PluginAutoConfiguration {
};
pluginManager.setExactVersionAllowed(pluginProperties.isExactVersionAllowed());
pluginManager.setSystemVersion(pluginProperties.getSystemVersion());
// only for development mode
if (RuntimeMode.DEPLOYMENT.equals(pluginManager.getRuntimeMode())) {
pluginManager.setSystemVersion(getSystemVersion());
}
return pluginManager;
}
String getSystemVersion() {
return systemVersionSupplier.get().getNormalVersion();
}
@Bean
public RouterFunction<ServerResponse> pluginJsBundleRoute(HaloPluginManager haloPluginManager,
WebProperties webProperties) {

View File

@ -71,9 +71,4 @@ public class PluginProperties {
* Allows providing custom plugin loaders.
*/
private Class<PluginLoader> customPluginLoader;
/**
* The system version used for comparisons to the plugin requires attribute.
*/
private String systemVersion = "0.0.0";
}

View File

@ -1,5 +1,6 @@
# Title definitions
problemDetail.title.org.springframework.web.server.ServerWebInputException=Bad Request
problemDetail.title.run.halo.app.infra.exception.UnsatisfiedAttributeValueException=Unsatisfied Request Attribute value
problemDetail.title.org.springframework.web.server.UnsupportedMediaTypeStatusException=Unsupported Media Type
problemDetail.title.org.springframework.web.server.MissingRequestValueException=Missing Request Value
problemDetail.title.org.springframework.web.server.UnsatisfiedRequestParameterException=Unsatisfied Request Parameter
@ -34,3 +35,5 @@ problemDetail.theme.upgrade.nameMismatch=The current theme name {0} did not matc
problemDetail.theme.install.missingManifest=Missing theme manifest file "theme.yaml" or "theme.yml".
problemDetail.theme.install.alreadyExists=Theme {0} already exists.
problemDetail.directoryTraversal=Directory traversal detected. Base path is {0}, but real path is {1}.
problemDetail.plugin.version.unsatisfied.requires=Plugin requires a minimum system version of {0}, but the current version is {1}.

View File

@ -1,4 +1,7 @@
problemDetail.title.org.springframework.web.server.ServerWebInputException=请求参数有误
problemDetail.title.run.halo.app.infra.exception.UnsatisfiedAttributeValueException=请求参数属性值不满足要求
problemDetail.title.run.halo.app.infra.exception.AttachmentAlreadyExistsException=附件已存在
problemDetail.run.halo.app.infra.exception.AttachmentAlreadyExistsException=文件 {0} 已存在,建议更名后重试。
problemDetail.plugin.version.unsatisfied.requires=插件要求一个最小的系统版本为 {0}, 但当前版本为 {1}。

View File

@ -13,6 +13,7 @@ import static org.mockito.Mockito.when;
import static org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
import static org.springframework.web.reactive.function.BodyInserters.fromMultipartData;
import com.github.zafarkhaja.semver.Version;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
@ -40,6 +41,7 @@ import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ListResult;
import run.halo.app.extension.Metadata;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.infra.SystemVersionSupplier;
import run.halo.app.infra.utils.FileUtils;
import run.halo.app.plugin.PluginProperties;
@ -53,6 +55,9 @@ class PluginEndpointTest {
@Mock
private ReactiveExtensionClient client;
@Mock
SystemVersionSupplier systemVersionSupplier;
@InjectMocks
PluginEndpoint endpoint;
@ -200,7 +205,7 @@ class PluginEndpointTest {
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
.build();
lenient().when(systemVersionSupplier.get()).thenReturn(Version.valueOf("0.0.0"));
tempDirectory = Files.createTempDirectory("halo-test-plugin-upgrade-");
lenient().when(pluginProperties.getPluginsRoot())
.thenReturn(tempDirectory.resolve("plugins").toString());

View File

@ -60,7 +60,8 @@ class PluginReconcilerTest {
@BeforeEach
void setUp() {
pluginReconciler = new PluginReconciler(extensionClient, haloPluginManager);
lenient().when(haloPluginManager.validatePluginVersion(any())).thenReturn(true);
lenient().when(haloPluginManager.getSystemVersion()).thenReturn("0.0.0");
lenient().when(haloPluginManager.getPlugin(any())).thenReturn(pluginWrapper);
lenient().when(haloPluginManager.getUnresolvedPlugins()).thenReturn(List.of());
}
@ -127,7 +128,7 @@ class PluginReconcilerTest {
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
verify(extensionClient, times(3)).update(any(Plugin.class));
verify(extensionClient, times(4)).update(any(Plugin.class));
Plugin updateArgs = pluginCaptor.getValue();
assertThat(updateArgs).isNotNull();
@ -162,7 +163,7 @@ class PluginReconcilerTest {
when(pluginWrapper.getPluginState()).thenReturn(PluginState.STARTED);
ArgumentCaptor<Plugin> pluginCaptor = doReconcileWithoutRequeue();
verify(extensionClient, times(3)).update(any(Plugin.class));
verify(extensionClient, times(4)).update(any(Plugin.class));
Plugin updateArgs = pluginCaptor.getValue();
assertThat(updateArgs).isNotNull();

View File

@ -0,0 +1,60 @@
package run.halo.app.infra;
import static org.assertj.core.api.Assertions.assertThat;
import com.github.zafarkhaja.semver.Version;
import java.util.Properties;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.info.BuildProperties;
/**
* Tests for {@link DefaultSystemVersionSupplier}.
*
* @author guqing
* @since 2.0.0
*/
class DefaultSystemVersionSupplierTest {
private DefaultSystemVersionSupplier systemVersionSupplier;
@BeforeEach
void setUp() {
systemVersionSupplier = new DefaultSystemVersionSupplier();
}
@Test
void getWhenBuildPropertiesNotSet() {
Version version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("0.0.0");
}
@Test
void getWhenBuildPropertiesButVersionIsNull() {
Properties properties = new Properties();
BuildProperties buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties);
Version version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("0.0.0");
}
@Test
void getWhenBuildPropertiesAndVersionNotEmpty() {
Properties properties = new Properties();
properties.put("version", "2.0.0");
BuildProperties buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties);
Version version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("2.0.0");
properties.put("version", "2.0.0-SNAPSHOT");
buildProperties = new BuildProperties(properties);
systemVersionSupplier.setBuildProperties(buildProperties);
version = systemVersionSupplier.get();
assertThat(version.toString()).isEqualTo("2.0.0-SNAPSHOT");
assertThat(version.getPreReleaseVersion()).isEqualTo("SNAPSHOT");
}
}

View File

@ -0,0 +1,51 @@
package run.halo.app.infra.utils;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link VersionUtils}.
*
* @author guqing
* @since 2.2.0
*/
class VersionUtilsTest {
@Test
void satisfiesRequires() {
// match all requires
String systemVersion = "0.0.0";
String requires = ">=2.2.0";
boolean result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isTrue();
systemVersion = "2.0.0";
requires = "*";
result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isTrue();
systemVersion = "2.0.0";
requires = "";
result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isTrue();
// match exact version
systemVersion = "2.0.0";
requires = ">=2.0.0";
result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isTrue();
systemVersion = "2.0.0";
requires = ">2.0.0";
result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isFalse();
//an exact version x.y.z will implicitly mean the same as >=x.y.z
systemVersion = "2.1.0";
// means >=2.0.0
requires = "2.0.0";
result = VersionUtils.satisfiesRequires(systemVersion, requires);
assertThat(result).isTrue();
}
}