diff --git a/build.gradle b/build.gradle index b77241ad9..2b8ad9351 100644 --- a/build.gradle +++ b/build.gradle @@ -27,6 +27,10 @@ configurations { } } +springBoot { + buildInfo() +} + bootJar { manifest { attributes "Implementation-Title": "Halo Application", diff --git a/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java index 80f574d4a..82532f93f 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/PluginEndpoint.java @@ -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 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 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()) diff --git a/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java b/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java index aff8fa340..8b38380c8 100644 --- a/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java +++ b/src/main/java/run/halo/app/core/extension/reconciler/PluginReconciler.java @@ -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 { 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 { 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 { }); } + 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 { 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()); } } diff --git a/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java b/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java new file mode 100644 index 000000000..c29c34ab4 --- /dev/null +++ b/src/main/java/run/halo/app/infra/DefaultSystemVersionSupplier.java @@ -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); + } +} diff --git a/src/main/java/run/halo/app/infra/SystemVersionSupplier.java b/src/main/java/run/halo/app/infra/SystemVersionSupplier.java new file mode 100644 index 000000000..c00184a1a --- /dev/null +++ b/src/main/java/run/halo/app/infra/SystemVersionSupplier.java @@ -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 Semantic Versioning 2.0.0 + * @since 2.0.0 + */ +public interface SystemVersionSupplier extends Supplier { +} diff --git a/src/main/java/run/halo/app/infra/exception/UnsatisfiedAttributeValueException.java b/src/main/java/run/halo/app/infra/exception/UnsatisfiedAttributeValueException.java new file mode 100644 index 000000000..4b74dd998 --- /dev/null +++ b/src/main/java/run/halo/app/infra/exception/UnsatisfiedAttributeValueException.java @@ -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); + } +} diff --git a/src/main/java/run/halo/app/infra/utils/VersionUtils.java b/src/main/java/run/halo/app/infra/utils/VersionUtils.java new file mode 100644 index 000000000..09441ac08 --- /dev/null +++ b/src/main/java/run/halo/app/infra/utils/VersionUtils.java @@ -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 + * semver-expressions-api-ranges 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); + } + } +} diff --git a/src/main/java/run/halo/app/plugin/HaloPluginManager.java b/src/main/java/run/halo/app/plugin/HaloPluginManager.java index 97dfb673c..17481154c 100644 --- a/src/main/java/run/halo/app/plugin/HaloPluginManager.java +++ b/src/main/java/run/halo/app/plugin/HaloPluginManager.java @@ -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); diff --git a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java index 0263df555..40d4df246 100644 --- a/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java +++ b/src/main/java/run/halo/app/plugin/PluginAutoConfiguration.java @@ -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 pluginJsBundleRoute(HaloPluginManager haloPluginManager, WebProperties webProperties) { diff --git a/src/main/java/run/halo/app/plugin/PluginProperties.java b/src/main/java/run/halo/app/plugin/PluginProperties.java index 7e63b0b26..1e26442f2 100644 --- a/src/main/java/run/halo/app/plugin/PluginProperties.java +++ b/src/main/java/run/halo/app/plugin/PluginProperties.java @@ -71,9 +71,4 @@ public class PluginProperties { * Allows providing custom plugin loaders. */ private Class customPluginLoader; - - /** - * The system version used for comparisons to the plugin requires attribute. - */ - private String systemVersion = "0.0.0"; } diff --git a/src/main/resources/config/i18n/messages.properties b/src/main/resources/config/i18n/messages.properties index 127f17899..77c0d267b 100644 --- a/src/main/resources/config/i18n/messages.properties +++ b/src/main/resources/config/i18n/messages.properties @@ -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}. diff --git a/src/main/resources/config/i18n/messages_zh.properties b/src/main/resources/config/i18n/messages_zh.properties index 22682218d..c7fd50708 100644 --- a/src/main/resources/config/i18n/messages_zh.properties +++ b/src/main/resources/config/i18n/messages_zh.properties @@ -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}。 diff --git a/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java index c9bff1ff2..c6aed940d 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/PluginEndpointTest.java @@ -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()); diff --git a/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java b/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java index a43ddfa1f..53d460752 100644 --- a/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java +++ b/src/test/java/run/halo/app/core/extension/reconciler/PluginReconcilerTest.java @@ -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 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 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(); diff --git a/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java b/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java new file mode 100644 index 000000000..b323a8d96 --- /dev/null +++ b/src/test/java/run/halo/app/infra/DefaultSystemVersionSupplierTest.java @@ -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"); + } +} \ No newline at end of file diff --git a/src/test/java/run/halo/app/infra/utils/VersionUtilsTest.java b/src/test/java/run/halo/app/infra/utils/VersionUtilsTest.java new file mode 100644 index 000000000..fa02a8ac2 --- /dev/null +++ b/src/test/java/run/halo/app/infra/utils/VersionUtilsTest.java @@ -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(); + } +}