mirror of https://github.com/halo-dev/halo
Provide an endpoint to upgrade plugin (#2624)
#### What type of PR is this? /kind feature /area core /milestone 2.0 #### What this PR does / why we need it: Provide an endpoint to upgrade plugin by uploading a new jar file. #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/2551 #### Special notes for your reviewer: 1. Install an old plugin 2. Update the plugin and package again 3. Upgrade the plugin and see the result #### Does this PR introduce a user-facing change? ```release-note 提供插件更新功能 ```pull/2632/head
parent
3ec7e31cac
commit
d9cb6bf732
|
@ -1,8 +1,10 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static java.nio.file.Files.copy;
|
||||
import static java.util.Comparator.comparing;
|
||||
import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
|
||||
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
|
||||
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
|
||||
import static org.springdoc.core.fn.builders.requestbody.Builder.requestBodyBuilder;
|
||||
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
|
||||
import static org.springframework.boot.convert.ApplicationConversionService.getSharedInstance;
|
||||
|
@ -10,33 +12,40 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
|
|||
import static run.halo.app.extension.ListResult.generateGenericClass;
|
||||
import static run.halo.app.extension.router.QueryParamBuildUtil.buildParametersFromType;
|
||||
import static run.halo.app.extension.router.selector.SelectorUtil.labelAndFieldSelectorToPredicate;
|
||||
import static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.ArraySchema;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Predicate;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.MediaType;
|
||||
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.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import org.springframework.web.server.ServerErrorException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebInputException;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.scheduler.Schedulers;
|
||||
import reactor.util.retry.Retry;
|
||||
import run.halo.app.core.extension.Plugin;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.IListRequest.QueryListRequest;
|
||||
|
@ -73,6 +82,16 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
))
|
||||
.response(responseBuilder().implementation(Plugin.class))
|
||||
)
|
||||
.POST("plugins/{name}/upgrade", contentType(MediaType.MULTIPART_FORM_DATA),
|
||||
this::upgrade, builder -> builder.operationId("UpgradePlugin")
|
||||
.description("Upgrade a plugin by uploading a Jar file")
|
||||
.tag(tag)
|
||||
.parameter(parameterBuilder().name("name").in(ParameterIn.PATH).required(true))
|
||||
.requestBody(requestBodyBuilder()
|
||||
.required(true)
|
||||
.content(contentBuilder().mediaType(MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
.schema(schemaBuilder().implementation(InstallRequest.class))))
|
||||
)
|
||||
.GET("plugins", this::list, builder -> {
|
||||
builder.operationId("ListPlugins")
|
||||
.tag(tag)
|
||||
|
@ -83,6 +102,78 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> upgrade(ServerRequest request) {
|
||||
var pluginNameInPath = request.pathVariable("name");
|
||||
var tempDirRef = new AtomicReference<Path>();
|
||||
var tempPluginPathRef = new AtomicReference<Path>();
|
||||
return client.fetch(Plugin.class, pluginNameInPath)
|
||||
.switchIfEmpty(Mono.error(() -> new ServerWebInputException(
|
||||
"The given plugin with name " + pluginNameInPath + " does not exit")))
|
||||
.then(request.multipartData())
|
||||
.flatMap(this::getJarFilePart)
|
||||
.flatMap(jarFilePart -> createTempDirectory()
|
||||
.doOnNext(tempDirRef::set)
|
||||
.flatMap(tempDirectory -> {
|
||||
var pluginPath = tempDirectory.resolve(jarFilePart.filename());
|
||||
return jarFilePart.transferTo(pluginPath).thenReturn(pluginPath);
|
||||
}))
|
||||
.doOnNext(tempPluginPathRef::set)
|
||||
.map(pluginPath -> new YamlPluginFinder().find(pluginPath))
|
||||
.doOnNext(newPlugin -> {
|
||||
// validate the plugin name
|
||||
if (!Objects.equals(pluginNameInPath, newPlugin.getMetadata().getName())) {
|
||||
throw new ServerWebInputException(
|
||||
"The uploaded plugin doesn't match the given plugin name");
|
||||
}
|
||||
})
|
||||
.flatMap(newPlugin -> deletePluginAndWaitForComplete(newPlugin.getMetadata().getName())
|
||||
.map(oldPlugin -> {
|
||||
var enabled = oldPlugin.getSpec().getEnabled();
|
||||
newPlugin.getSpec().setEnabled(enabled);
|
||||
return newPlugin;
|
||||
})
|
||||
)
|
||||
.publishOn(Schedulers.boundedElastic())
|
||||
.doOnNext(newPlugin -> {
|
||||
// copy the Jar file into plugin root
|
||||
try {
|
||||
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);
|
||||
}
|
||||
})
|
||||
.flatMap(client::create)
|
||||
.flatMap(newPlugin -> ServerResponse.ok().bodyValue(newPlugin))
|
||||
.doFinally(signalType -> deleteRecursivelyAndSilently(tempDirRef.get()));
|
||||
}
|
||||
|
||||
private Mono<Plugin> deletePluginAndWaitForComplete(String pluginName) {
|
||||
return client.fetch(Plugin.class, pluginName)
|
||||
.flatMap(client::delete)
|
||||
.flatMap(plugin -> waitForDeleted(plugin.getMetadata().getName()).thenReturn(plugin));
|
||||
}
|
||||
|
||||
private Mono<Void> waitForDeleted(String pluginName) {
|
||||
return client.fetch(Plugin.class, pluginName)
|
||||
.flatMap(plugin -> Mono.error(
|
||||
new RetryException("Re-check if the plugin is deleted successfully")))
|
||||
.retryWhen(Retry.fixedDelay(20, Duration.ofMillis(100))
|
||||
.filter(t -> t instanceof RetryException)
|
||||
)
|
||||
.onErrorMap(Exceptions::isRetryExhausted,
|
||||
t -> new ServerErrorException("Wait timeout for plugin deleted", t))
|
||||
.then();
|
||||
}
|
||||
|
||||
private Mono<Path> createTempDirectory() {
|
||||
return Mono.fromCallable(() -> Files.createTempDirectory("halo-plugin-"))
|
||||
.subscribeOn(Schedulers.boundedElastic());
|
||||
}
|
||||
|
||||
public static class ListRequest extends QueryListRequest {
|
||||
|
||||
private final ServerWebExchange exchange;
|
||||
|
@ -109,7 +200,7 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
+ "creationTimestamp"),
|
||||
schema = @Schema(description = "like field,asc or field,desc",
|
||||
implementation = String.class,
|
||||
example = "creationtimestamp,desc"))
|
||||
example = "creationTimestamp,desc"))
|
||||
public Sort getSort() {
|
||||
return SortResolver.defaultInstance.resolve(exchange);
|
||||
}
|
||||
|
@ -183,8 +274,7 @@ public class PluginEndpoint implements CustomEndpoint {
|
|||
}
|
||||
|
||||
Mono<ServerResponse> install(ServerRequest request) {
|
||||
return request.bodyToMono(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {
|
||||
})
|
||||
return request.multipartData()
|
||||
.flatMap(this::getJarFilePart)
|
||||
.flatMap(this::transferToTemp)
|
||||
.flatMap(tempJarFilePath -> {
|
||||
|
|
|
@ -6,13 +6,19 @@ import java.io.Closeable;
|
|||
import java.io.IOException;
|
||||
import java.nio.file.CopyOption;
|
||||
import java.nio.file.DirectoryNotEmptyException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
|
@ -65,6 +71,42 @@ public abstract class FileUtils {
|
|||
}
|
||||
}
|
||||
|
||||
public static void zip(Path sourcePath, Path targetPath) throws IOException {
|
||||
try (var zos = new ZipOutputStream(Files.newOutputStream(targetPath))) {
|
||||
Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
checkDirectoryTraversal(sourcePath, file);
|
||||
var relativePath = sourcePath.relativize(file);
|
||||
var entry = new ZipEntry(relativePath.toString());
|
||||
zos.putNextEntry(entry);
|
||||
Files.copy(file, zos);
|
||||
zos.closeEntry();
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void jar(Path sourcePath, Path targetPath) throws IOException {
|
||||
try (var jos = new JarOutputStream(Files.newOutputStream(targetPath))) {
|
||||
Files.walkFileTree(sourcePath, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
checkDirectoryTraversal(sourcePath, file);
|
||||
var relativePath = sourcePath.relativize(file);
|
||||
var entry = new JarEntry(relativePath.toString());
|
||||
jos.putNextEntry(entry);
|
||||
Files.copy(file, jos);
|
||||
jos.closeEntry();
|
||||
return super.visitFile(file, attrs);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates directories if absent.
|
||||
*
|
||||
|
|
|
@ -15,7 +15,7 @@ rules:
|
|||
- apiGroups: [ "plugin.halo.run" ]
|
||||
resources: [ "plugins" ]
|
||||
verbs: [ "create", "patch", "update", "delete", "deletecollection" ]
|
||||
- nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/plugins/install" ]
|
||||
- nonResourceURLs: [ "/apis/api.console.halo.run/v1alpha1/plugins/*" ]
|
||||
verbs: [ "create" ]
|
||||
---
|
||||
apiVersion: v1alpha1
|
||||
|
|
|
@ -1,29 +1,49 @@
|
|||
package run.halo.app.core.extension.endpoint;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.ArgumentMatchers.same;
|
||||
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 org.springframework.test.web.reactive.server.WebTestClient.bindToRouterFunction;
|
||||
import static org.springframework.web.reactive.function.BodyInserters.fromMultipartData;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
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 org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.MultipartBodyBuilder;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
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.utils.FileUtils;
|
||||
import run.halo.app.plugin.PluginProperties;
|
||||
|
||||
@Slf4j
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PluginEndpointTest {
|
||||
|
||||
|
@ -36,6 +56,9 @@ class PluginEndpointTest {
|
|||
@InjectMocks
|
||||
PluginEndpoint endpoint;
|
||||
|
||||
@Nested
|
||||
class PluginListTest {
|
||||
|
||||
@Test
|
||||
void shouldListEmptyPluginsWhenNoPlugins() {
|
||||
when(client.list(same(Plugin.class), any(), any(), anyInt(), anyInt()))
|
||||
|
@ -76,7 +99,8 @@ class PluginEndpointTest {
|
|||
void shouldFilterPluginsWhenKeywordProvided() {
|
||||
var expectPlugin =
|
||||
createPlugin("fake-plugin-2", "expected display name", "", false);
|
||||
var unexpectedPlugin1 = createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin1 =
|
||||
createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin2 =
|
||||
createPlugin("fake-plugin-3", "second fake display name", "", false);
|
||||
var plugins = List.of(
|
||||
|
@ -103,7 +127,8 @@ class PluginEndpointTest {
|
|||
void shouldFilterPluginsWhenEnabledProvided() {
|
||||
var expectPlugin =
|
||||
createPlugin("fake-plugin-2", "expected display name", "", true);
|
||||
var unexpectedPlugin1 = createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin1 =
|
||||
createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin2 =
|
||||
createPlugin("fake-plugin-3", "second fake display name", "", false);
|
||||
var plugins = List.of(
|
||||
|
@ -130,7 +155,8 @@ class PluginEndpointTest {
|
|||
void shouldSortPluginsWhenCreationTimestampSet() {
|
||||
var expectPlugin =
|
||||
createPlugin("fake-plugin-2", "expected display name", "", true);
|
||||
var unexpectedPlugin1 = createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin1 =
|
||||
createPlugin("fake-plugin-1", "first fake display name", "", false);
|
||||
var unexpectedPlugin2 =
|
||||
createPlugin("fake-plugin-3", "second fake display name", "", false);
|
||||
var expectResult = new ListResult<>(List.of(expectPlugin));
|
||||
|
@ -158,6 +184,122 @@ class PluginEndpointTest {
|
|||
));
|
||||
}), anyInt(), anyInt());
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class PluginUpgradeTest {
|
||||
|
||||
WebTestClient webClient;
|
||||
|
||||
Path tempDirectory;
|
||||
|
||||
Path plugin002;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws URISyntaxException, IOException {
|
||||
webClient = WebTestClient.bindToRouterFunction(endpoint.endpoint())
|
||||
.build();
|
||||
|
||||
|
||||
tempDirectory = Files.createTempDirectory("halo-test-plugin-upgrade-");
|
||||
lenient().when(pluginProperties.getPluginsRoot())
|
||||
.thenReturn(tempDirectory.resolve("plugins").toString());
|
||||
plugin002 = tempDirectory.resolve("plugin-0.0.2.jar");
|
||||
|
||||
var plugin002Uri = requireNonNull(
|
||||
getClass().getClassLoader().getResource("plugin/plugin-0.0.2")).toURI();
|
||||
|
||||
FileUtils.jar(Paths.get(plugin002Uri), tempDirectory.resolve("plugin-0.0.2.jar"));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
FileUtils.deleteRecursivelyAndSilently(tempDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResponseBadRequestIfNoPluginInstalledBefore() {
|
||||
var bodyBuilder = new MultipartBodyBuilder();
|
||||
bodyBuilder.part("file", new FileSystemResource(plugin002))
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
when(client.fetch(Plugin.class, "fake-plugin")).thenReturn(Mono.empty());
|
||||
|
||||
webClient.post().uri("/plugins/fake-plugin/upgrade")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(fromMultipartData(bodyBuilder.build()))
|
||||
.exchange()
|
||||
.expectStatus().isBadRequest();
|
||||
|
||||
verify(client).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client, never()).delete(any(Plugin.class));
|
||||
verify(client, never()).create(any(Plugin.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldWaitTimeoutIfOldPluginCannotBeDeleted() {
|
||||
var bodyBuilder = new MultipartBodyBuilder();
|
||||
bodyBuilder.part("file", new FileSystemResource(plugin002))
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
var oldPlugin = createPlugin("fake-plugin");
|
||||
when(client.fetch(Plugin.class, "fake-plugin"))
|
||||
// for first check
|
||||
.thenReturn(Mono.just(oldPlugin))
|
||||
// for deleting check
|
||||
.thenReturn(Mono.just(oldPlugin))
|
||||
// for waiting
|
||||
.thenReturn(Mono.just(oldPlugin));
|
||||
|
||||
when(client.delete(oldPlugin)).thenReturn(Mono.just(oldPlugin));
|
||||
|
||||
webClient.post().uri("/plugins/fake-plugin/upgrade")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(fromMultipartData(bodyBuilder.build()))
|
||||
.exchange()
|
||||
.expectStatus().is5xxServerError();
|
||||
|
||||
verify(client, times(3)).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client).delete(oldPlugin);
|
||||
verify(client, never()).create(any(Plugin.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeOkIfPluginInstalledBefore() {
|
||||
var bodyBuilder = new MultipartBodyBuilder();
|
||||
bodyBuilder.part("file", new FileSystemResource(plugin002))
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA);
|
||||
|
||||
var oldPlugin = createPlugin("fake-plugin");
|
||||
when(client.fetch(Plugin.class, "fake-plugin"))
|
||||
// for first check
|
||||
.thenReturn(Mono.just(oldPlugin))
|
||||
// for deleting check
|
||||
.thenReturn(Mono.just(oldPlugin))
|
||||
// for waiting
|
||||
.thenReturn(Mono.empty());
|
||||
|
||||
when(client.delete(oldPlugin)).thenReturn(Mono.just(oldPlugin));
|
||||
|
||||
Plugin newPlugin = createPlugin("fake-plugin", Instant.now());
|
||||
when(client.<Plugin>create(
|
||||
argThat(plugin -> "0.0.2".equals(plugin.getSpec().getVersion()))))
|
||||
.thenReturn(Mono.just(newPlugin));
|
||||
|
||||
webClient.post().uri("/plugins/fake-plugin/upgrade")
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(fromMultipartData(bodyBuilder.build()))
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Plugin.class)
|
||||
.isEqualTo(newPlugin);
|
||||
|
||||
verify(client, times(3)).fetch(Plugin.class, "fake-plugin");
|
||||
verify(client).delete(oldPlugin);
|
||||
verify(client).create(any(Plugin.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Plugin createPlugin(String name) {
|
||||
return createPlugin(name, "fake display name", "fake description", null);
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class FileNameUtilsTest {
|
||||
|
||||
@Test
|
||||
public void shouldNotRemovExtIfNoExt() {
|
||||
public void shouldNotRemoveExtIfNoExt() {
|
||||
assertEquals("halo", FileNameUtils.removeFileExtension("halo", true));
|
||||
assertEquals("halo", FileNameUtils.removeFileExtension("halo", false));
|
||||
}
|
||||
|
@ -41,4 +42,10 @@ class FileNameUtilsTest {
|
|||
assertEquals(".halo", FileNameUtils.removeFileExtension(".halo.tar.gz", true));
|
||||
assertEquals(".halo.tar", FileNameUtils.removeFileExtension(".halo.tar.gz", false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnNullIfFilenameIsNull() {
|
||||
assertNull(FileNameUtils.removeFileExtension(null, true));
|
||||
assertNull(FileNameUtils.removeFileExtension(null, false));
|
||||
}
|
||||
}
|
|
@ -1,8 +1,23 @@
|
|||
package run.halo.app.infra.utils;
|
||||
|
||||
import static java.util.Objects.requireNonNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static run.halo.app.infra.utils.FileUtils.checkDirectoryTraversal;
|
||||
import static run.halo.app.infra.utils.FileUtils.deleteRecursivelyAndSilently;
|
||||
import static run.halo.app.infra.utils.FileUtils.jar;
|
||||
import static run.halo.app.infra.utils.FileUtils.unzip;
|
||||
import static run.halo.app.infra.utils.FileUtils.zip;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.zip.ZipInputStream;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import run.halo.app.infra.exception.AccessDeniedException;
|
||||
|
@ -30,4 +45,64 @@ class FileUtilsTest {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ZipTest {
|
||||
|
||||
Path tempDirectory;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws IOException {
|
||||
tempDirectory = Files.createTempDirectory("halo-test-fileutils-zip-");
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanUp() {
|
||||
deleteRecursivelyAndSilently(tempDirectory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zipFolderAndUnzip() throws IOException, URISyntaxException {
|
||||
var uri = requireNonNull(getClass().getClassLoader().getResource("folder-to-zip"))
|
||||
.toURI();
|
||||
var zipPath = tempDirectory.resolve("example.zip");
|
||||
zip(Paths.get(uri), zipPath);
|
||||
|
||||
var unzipTarget = tempDirectory.resolve("example-folder");
|
||||
try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {
|
||||
unzip(zis, unzipTarget);
|
||||
}
|
||||
|
||||
var content = Files.readString(unzipTarget.resolve("examplefile"));
|
||||
assertEquals("Here is an example file.\n", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jarFolderAndUnzip() throws IOException, URISyntaxException {
|
||||
var uri = requireNonNull(getClass().getClassLoader().getResource("folder-to-zip"))
|
||||
.toURI();
|
||||
var zipPath = tempDirectory.resolve("example.zip");
|
||||
jar(Paths.get(uri), zipPath);
|
||||
|
||||
var unzipTarget = tempDirectory.resolve("example-folder");
|
||||
try (var zis = new ZipInputStream(Files.newInputStream(zipPath))) {
|
||||
unzip(zis, unzipTarget);
|
||||
}
|
||||
|
||||
var content = Files.readString(unzipTarget.resolve("examplefile"));
|
||||
assertEquals("Here is an example file.\n", content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void zipFolderIfNoSuchFolder() {
|
||||
assertThrows(NoSuchFileException.class, () ->
|
||||
zip(Paths.get("no-such-folder"), tempDirectory.resolve("example.zip")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void jarFolderIfNoSuchFolder() {
|
||||
assertThrows(NoSuchFileException.class, () ->
|
||||
jar(Paths.get("no-such-folder"), tempDirectory.resolve("example.zip")));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package run.halo.app.security;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
@ -65,7 +64,7 @@ class DefaultUserDetailServiceTest {
|
|||
void shouldReturnErrorWhenFailedToUpdatePassword() {
|
||||
var fakeUser = createFakeUserDetails();
|
||||
|
||||
var exception = mock(RuntimeException.class);
|
||||
var exception = new RuntimeException("failed to update password");
|
||||
when(userService.updatePassword("faker", "new-fake-password")).thenReturn(
|
||||
Mono.error(exception)
|
||||
);
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Here is an example file.
|
|
@ -0,0 +1,15 @@
|
|||
apiVersion: v1
|
||||
kind: Plugin
|
||||
metadata:
|
||||
name: fake-plugin
|
||||
spec:
|
||||
# 'version' is a valid semantic version string (see semver.org).
|
||||
version: 0.0.2
|
||||
requires: ">=2.0.0"
|
||||
author: johnniang
|
||||
logo: https://halo.run/avatar
|
||||
homepage: https://github.com/halo-sigs/halo-plugin-1
|
||||
displayName: "Fake Display Name"
|
||||
description: "Fake description"
|
||||
license:
|
||||
- name: GPLv3
|
Loading…
Reference in New Issue