mirror of https://github.com/halo-dev/halo
Add support for Extension mechanism (#2086)
* Add support for Extension mechanism 1. Add ExtensionStore 2. Add Extension 3. Add Scheme * Add more unit tests * Fix checkstyle violations * Remove unused importpull/2093/head
parent
be2c0654a2
commit
264f9e39cb
|
@ -0,0 +1,43 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractExtension contains basic structure of Extension and implements the Extension interface.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public abstract class AbstractExtension implements Extension {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String apiVersion;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String kind;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private Metadata metadata;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void groupVersionKind(GroupVersionKind gvk) {
|
||||||
|
this.apiVersion = gvk.groupVersion().toString();
|
||||||
|
this.kind = gvk.kind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GroupVersionKind groupVersionKind() {
|
||||||
|
return GroupVersionKind.fromAPIVersionAndKind(this.apiVersion, this.kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void metadata(Metadata metadata) {
|
||||||
|
this.metadata = metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Metadata metadata() {
|
||||||
|
return this.metadata;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.data.support.PageableExecutionUtils;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DefaultExtensionClient is default implementation of ExtensionClient.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class DefaultExtensionClient implements ExtensionClient {
|
||||||
|
|
||||||
|
private final ExtensionStoreClient storeClient;
|
||||||
|
private final ExtensionConverter converter;
|
||||||
|
|
||||||
|
public DefaultExtensionClient(ExtensionStoreClient storeClient, ExtensionConverter converter) {
|
||||||
|
this.storeClient = storeClient;
|
||||||
|
this.converter = converter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> List<E> list(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparator) {
|
||||||
|
var scheme = Schemes.INSTANCE.get(type);
|
||||||
|
var storeNamePrefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
||||||
|
|
||||||
|
var storesStream = storeClient.listByNamePrefix(storeNamePrefix).stream()
|
||||||
|
.map(extensionStore -> converter.convertFrom(type, extensionStore));
|
||||||
|
if (predicate != null) {
|
||||||
|
storesStream = storesStream.filter(predicate);
|
||||||
|
}
|
||||||
|
if (comparator != null) {
|
||||||
|
storesStream = storesStream.sorted(comparator);
|
||||||
|
}
|
||||||
|
return storesStream.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Page<E> page(Class<E> type, Predicate<E> predicate,
|
||||||
|
Comparator<E> comparators, int page, int size) {
|
||||||
|
var pageable = PageRequest.of(page, size);
|
||||||
|
var all = list(type, predicate, comparators);
|
||||||
|
var total = all.size();
|
||||||
|
var content =
|
||||||
|
all.stream().limit(pageable.getPageSize()).skip(pageable.getOffset()).toList();
|
||||||
|
return PageableExecutionUtils.getPage(content, pageable, () -> total);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> Optional<E> fetch(Class<E> type, String name) {
|
||||||
|
var scheme = Schemes.INSTANCE.get(type);
|
||||||
|
|
||||||
|
var storeName = ExtensionUtil.buildStoreName(scheme, name);
|
||||||
|
return storeClient.fetchByName(storeName)
|
||||||
|
.map(extensionStore -> converter.convertFrom(type, extensionStore));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> void create(E extension) {
|
||||||
|
extension.metadata().setCreationTimestamp(Instant.now());
|
||||||
|
var extensionStore = converter.convertTo(extension);
|
||||||
|
storeClient.create(extensionStore.getName(), extensionStore.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> void update(E extension) {
|
||||||
|
var extensionStore = converter.convertTo(extension);
|
||||||
|
Assert.notNull(extension.metadata().getVersion(),
|
||||||
|
"Extension version must not be null when updating");
|
||||||
|
storeClient.update(extensionStore.getName(), extensionStore.getVersion(),
|
||||||
|
extensionStore.getData());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> void delete(E extension) {
|
||||||
|
ExtensionStore extensionStore = converter.convertTo(extension);
|
||||||
|
storeClient.delete(extensionStore.getName(), extensionStore.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension is an interface which represents an Extension. It contains setters and getters of
|
||||||
|
* GroupVersionKind and Metadata.
|
||||||
|
*/
|
||||||
|
public interface Extension {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets GroupVersionKind of the Extension.
|
||||||
|
*
|
||||||
|
* @param gvk is GroupVersionKind data.
|
||||||
|
*/
|
||||||
|
void groupVersionKind(GroupVersionKind gvk);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets GroupVersionKind of the Extension.
|
||||||
|
*
|
||||||
|
* @return GroupVersionKind of the Extension.
|
||||||
|
*/
|
||||||
|
GroupVersionKind groupVersionKind();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets metadata of the Extension.
|
||||||
|
*
|
||||||
|
* @param metadata metadata of the Extension.
|
||||||
|
*/
|
||||||
|
void metadata(Metadata metadata);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets metadata of the Extension.
|
||||||
|
*
|
||||||
|
* @return metadata of the Extension.
|
||||||
|
*/
|
||||||
|
Metadata metadata();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Predicate;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionClient is an interface which contains some operations on Extension instead of
|
||||||
|
* ExtensionStore.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public interface ExtensionClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists Extensions by Extension type, filter and sorter.
|
||||||
|
*
|
||||||
|
* @param type is the class type of Extension.
|
||||||
|
* @param predicate filters the result.
|
||||||
|
* @param comparator sorts the result.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return all filtered and sorted Extensions.
|
||||||
|
*/
|
||||||
|
<E extends Extension> List<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 result.
|
||||||
|
* @param comparator sorts the result.
|
||||||
|
* @param page is page number which starts from 0.
|
||||||
|
* @param size is page size.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return a page of Extensions.
|
||||||
|
*/
|
||||||
|
<E extends Extension> Page<E> page(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> Optional<E> fetch(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> void 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> void 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> void delete(E extension);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionConverter contains bidirectional conversions between Extension and ExtensionStore.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public interface ExtensionConverter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Extension to ExtensionStore.
|
||||||
|
*
|
||||||
|
* @param extension is an Extension to be converted.
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return an ExtensionStore.
|
||||||
|
*/
|
||||||
|
<E extends Extension> ExtensionStore convertTo(E extension);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts Extension from ExtensionStore.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @param extensionStore is an ExtensionStore
|
||||||
|
* @param <E> is Extension type.
|
||||||
|
* @return an Extension
|
||||||
|
*/
|
||||||
|
<E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension utilities.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public final class ExtensionUtil {
|
||||||
|
|
||||||
|
private ExtensionUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the name prefix of ExtensionStore.
|
||||||
|
*
|
||||||
|
* @param scheme is scheme of an Extension.
|
||||||
|
* @return name prefix of ExtensionStore.
|
||||||
|
*/
|
||||||
|
public static String buildStoreNamePrefix(Scheme scheme) {
|
||||||
|
// rule of key: /registry/[group]/plural-name/extension-name
|
||||||
|
StringBuilder builder = new StringBuilder("/registry/");
|
||||||
|
if (StringUtils.hasText(scheme.groupVersionKind().group())) {
|
||||||
|
builder.append(scheme.groupVersionKind().group()).append('/');
|
||||||
|
}
|
||||||
|
builder.append(scheme.plural());
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds full name of ExtensionStore.
|
||||||
|
*
|
||||||
|
* @param scheme is scheme of an Extension.
|
||||||
|
* @param name the exact name of Extension.
|
||||||
|
* @return full name of ExtensionStore.
|
||||||
|
*/
|
||||||
|
public static String buildStoreName(Scheme scheme, String name) {
|
||||||
|
return buildStoreNamePrefix(scheme) + "/" + name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GVK is an annotation to specific metadata of Extension.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
public @interface GVK {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return group name of Extension.
|
||||||
|
*/
|
||||||
|
String group();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return version name of Extension.
|
||||||
|
*/
|
||||||
|
String version();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return kind name of Extension.
|
||||||
|
*/
|
||||||
|
String kind();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return plural name of Extension.
|
||||||
|
*/
|
||||||
|
String plural();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return singular name of Extension.
|
||||||
|
*/
|
||||||
|
String singular();
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroupKind contains group and kind data only.
|
||||||
|
*
|
||||||
|
* @param group is group name of Extension.
|
||||||
|
* @param kind is kind name of Extension.
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public record GroupKind(String group, String kind) {
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroupVersion contains group and version name of an Extension only.
|
||||||
|
*
|
||||||
|
* @param group is group name of Extension.
|
||||||
|
* @param version is version name of Extension.
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public record GroupVersion(String group, String version) {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return StringUtils.hasText(group) ? group + "/" + version : version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses APIVersion into GroupVersion record.
|
||||||
|
*
|
||||||
|
* @param apiVersion must not be blank.
|
||||||
|
* 1. If the given apiVersion does not contain any "/", we treat the group is empty.
|
||||||
|
* 2. If the given apiVersion contains more than 1 "/", we will throw an
|
||||||
|
* IllegalArgumentException.
|
||||||
|
* @return record contains group and version.
|
||||||
|
*/
|
||||||
|
public static GroupVersion parseAPIVersion(String apiVersion) {
|
||||||
|
Assert.hasText(apiVersion, "API version must not be blank");
|
||||||
|
|
||||||
|
var groupVersion = apiVersion.split("/");
|
||||||
|
return switch (groupVersion.length) {
|
||||||
|
case 1 -> new GroupVersion("", apiVersion);
|
||||||
|
case 2 -> new GroupVersion(groupVersion[0], groupVersion[1]);
|
||||||
|
default ->
|
||||||
|
throw new IllegalArgumentException("Unexpected APIVersion string: " + apiVersion);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GroupVersionKind contains group, version and kind name of an Extension.
|
||||||
|
*
|
||||||
|
* @param group is group name of Extension.
|
||||||
|
* @param version is version name of Extension.
|
||||||
|
* @param kind is kind name of Extension.
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public record GroupVersionKind(String group, String version, String kind) {
|
||||||
|
|
||||||
|
public GroupVersionKind {
|
||||||
|
Assert.hasText(version, "Version must not be blank");
|
||||||
|
Assert.hasText(kind, "Kind must not be blank");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets group and version name of Extension.
|
||||||
|
*
|
||||||
|
* @return group and version name of Extension.
|
||||||
|
*/
|
||||||
|
public GroupVersion groupVersion() {
|
||||||
|
return new GroupVersion(group, version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composes GroupVersionKind from API version and kind name.
|
||||||
|
*
|
||||||
|
* @param apiVersion is API version. Like "core.halo.run/v1alpha1"
|
||||||
|
* @param kind is kind name of Extension.
|
||||||
|
* @return GroupVersionKind of an Extension.
|
||||||
|
*/
|
||||||
|
public static GroupVersionKind fromAPIVersionAndKind(String apiVersion, String kind) {
|
||||||
|
Assert.hasText(kind, "Kind must not be blank");
|
||||||
|
|
||||||
|
var gv = GroupVersion.parseAPIVersion(apiVersion);
|
||||||
|
return new GroupVersionKind(gv.group(), gv.version(), kind);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON implementation of ExtensionConverter.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class JSONExtensionConverter implements ExtensionConverter {
|
||||||
|
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public JSONExtensionConverter(ObjectMapper objectMapper) {
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> ExtensionStore convertTo(E extension) {
|
||||||
|
var scheme = Schemes.INSTANCE.get(extension.getClass());
|
||||||
|
var storeName = ExtensionUtil.buildStoreName(scheme, extension.metadata().getName());
|
||||||
|
try {
|
||||||
|
// TODO Validate the extension in ExtensionClient
|
||||||
|
// keep converting
|
||||||
|
var data = objectMapper.writeValueAsBytes(extension);
|
||||||
|
var version = extension.metadata().getVersion();
|
||||||
|
return new ExtensionStore(storeName, data, version);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new ExtensionConvertException("Failed write Extension as bytes", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <E extends Extension> E convertFrom(Class<E> type, ExtensionStore extensionStore) {
|
||||||
|
try {
|
||||||
|
var extension = objectMapper.readValue(extensionStore.getData(), type);
|
||||||
|
extension.metadata().setVersion(extensionStore.getVersion());
|
||||||
|
return extension;
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new ExtensionConvertException("Failed to read Extension " + type + " from bytes",
|
||||||
|
e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata of Extension.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class Metadata {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metadata name. The name is unique globally.
|
||||||
|
*/
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Labels are like key-value format.
|
||||||
|
*/
|
||||||
|
private Map<String, String> labels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annotations are like key-value format.
|
||||||
|
*/
|
||||||
|
private Map<String, String> annotations;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current version of the Extension. It will be bumped up every update.
|
||||||
|
*/
|
||||||
|
private Long version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creation timestamp of the Extension.
|
||||||
|
*/
|
||||||
|
private Instant creationTimestamp;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletion timestamp of the Extension.
|
||||||
|
*/
|
||||||
|
private Instant deletionTimestamp;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents scheme of an Extension.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @param groupVersionKind is GroupVersionKind of Extension.
|
||||||
|
* @param plural is plural name of Extension.
|
||||||
|
* @param singular is singular name of Extension.
|
||||||
|
* @param jsonSchema is JSON schema of Extension.
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public record Scheme(Class<? extends Extension> type,
|
||||||
|
GroupVersionKind groupVersionKind,
|
||||||
|
String plural,
|
||||||
|
String singular,
|
||||||
|
ObjectNode jsonSchema) {
|
||||||
|
public Scheme {
|
||||||
|
Assert.notNull(type, "Type of Extension must not be null");
|
||||||
|
Assert.notNull(groupVersionKind, "GroupVersionKind of Extension must not be null");
|
||||||
|
Assert.hasText(plural, "Plural name of Extension must not be blank");
|
||||||
|
Assert.hasText(singular, "Singular name of Extension must not be blank");
|
||||||
|
|
||||||
|
//TODO Validate the json schema when we plan to integrate Extension validation.
|
||||||
|
// Assert.notNull(jsonSchema, "Json Schema must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,118 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import run.halo.app.extension.exception.ExtensionException;
|
||||||
|
import run.halo.app.extension.exception.SchemeNotFoundException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schemes is aggregation of schemes and responsible for managing and organizing schemes.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public enum Schemes {
|
||||||
|
|
||||||
|
INSTANCE;
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All registered schemes.
|
||||||
|
*/
|
||||||
|
private final Set<Scheme> schemes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map mapping type and scheme of Extension.
|
||||||
|
*/
|
||||||
|
private final Map<Class<? extends Extension>, Scheme> typeToScheme;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The map mapping GroupVersionKind and type of Extension.
|
||||||
|
*/
|
||||||
|
private final Map<GroupVersionKind, Class<? extends Extension>> gvkToType;
|
||||||
|
|
||||||
|
Schemes() {
|
||||||
|
schemes = new HashSet<>();
|
||||||
|
typeToScheme = new HashMap<>();
|
||||||
|
gvkToType = new HashMap<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear registered schemes.
|
||||||
|
* <p>
|
||||||
|
* This method is only for test.
|
||||||
|
*/
|
||||||
|
void clear() {
|
||||||
|
schemes.clear();
|
||||||
|
typeToScheme.clear();
|
||||||
|
gvkToType.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers an Extension using its type.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @param <T> Extension class.
|
||||||
|
*/
|
||||||
|
public <T extends Extension> void register(Class<T> type) {
|
||||||
|
// concrete scheme from annotation
|
||||||
|
var gvk = type.getAnnotation(GVK.class);
|
||||||
|
if (gvk == null) {
|
||||||
|
// should never happen
|
||||||
|
throw new ExtensionException(
|
||||||
|
String.format("Annotation %s needs to be on Extension %s", GVK.class.getName(),
|
||||||
|
type.getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Generate the JSON schema here
|
||||||
|
var scheme = new Scheme(type, new GroupVersionKind(gvk.group(), gvk.version(), gvk.kind()),
|
||||||
|
gvk.plural(), gvk.singular(), null);
|
||||||
|
|
||||||
|
register(scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registers a Scheme of Extension.
|
||||||
|
*
|
||||||
|
* @param scheme is fresh scheme of Extension.
|
||||||
|
*/
|
||||||
|
public void register(Scheme scheme) {
|
||||||
|
boolean added = schemes.add(scheme);
|
||||||
|
if (!added) {
|
||||||
|
logger.warn("Scheme " + scheme
|
||||||
|
+ " has been registered before, please check the repeat register.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
typeToScheme.put(scheme.type(), scheme);
|
||||||
|
gvkToType.put(scheme.groupVersionKind(), scheme.type());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a Scheme using Extension type.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @return an optional Scheme.
|
||||||
|
*/
|
||||||
|
public Optional<Scheme> fetch(Class<? extends Extension> type) {
|
||||||
|
return Optional.ofNullable(typeToScheme.get(type));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a scheme using Extension type.
|
||||||
|
*
|
||||||
|
* @param type is Extension type.
|
||||||
|
* @return non-null Extension scheme.
|
||||||
|
* @throws SchemeNotFoundException when the Extension is not found.
|
||||||
|
*/
|
||||||
|
public Scheme get(Class<? extends Extension> type) {
|
||||||
|
return fetch(type).orElseThrow(() -> new SchemeNotFoundException(
|
||||||
|
"Scheme was not found for Extension " + type.getSimpleName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package run.halo.app.extension.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionConvertException is thrown when an Extension conversion error occurs.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class ExtensionConvertException extends ExtensionException {
|
||||||
|
|
||||||
|
public ExtensionConvertException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionConvertException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionConvertException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionConvertException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionConvertException(String message, Throwable cause, boolean enableSuppression,
|
||||||
|
boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package run.halo.app.extension.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionException is the superclass of those exceptions that can be thrown by Extension module.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class ExtensionException extends RuntimeException {
|
||||||
|
|
||||||
|
public ExtensionException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionException(String message, Throwable cause, boolean enableSuppression,
|
||||||
|
boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package run.halo.app.extension.exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SchemeNotFoundException is thrown while we try to get a scheme but not found.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public class SchemeNotFoundException extends ExtensionException {
|
||||||
|
|
||||||
|
public SchemeNotFoundException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchemeNotFoundException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchemeNotFoundException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchemeNotFoundException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SchemeNotFoundException(String message, Throwable cause, boolean enableSuppression,
|
||||||
|
boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Lob;
|
||||||
|
import jakarta.persistence.Version;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtensionStore is an entity for storing Extension data into database.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Entity(name = "extensions")
|
||||||
|
public class ExtensionStore {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extension store name, which is globally unique.
|
||||||
|
* We will use it to query Extensions by using left-like query clause.
|
||||||
|
*/
|
||||||
|
@Id
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exactly Extension body, which might be base64 format.
|
||||||
|
*/
|
||||||
|
@Lob
|
||||||
|
private byte[] data;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This field only for serving optimistic lock value.
|
||||||
|
*/
|
||||||
|
@Version
|
||||||
|
private Long version;
|
||||||
|
|
||||||
|
public ExtensionStore() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionStore(String name, byte[] data) {
|
||||||
|
this.name = name;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionStore(String name, Long version) {
|
||||||
|
this.name = name;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExtensionStore(String name, byte[] data, Long version) {
|
||||||
|
this.name = name;
|
||||||
|
this.data = data;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to query and operate ExtensionStore.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
public interface ExtensionStoreClient {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all ExtensionStores by name prefix.
|
||||||
|
*
|
||||||
|
* @param prefix is the prefix of ExtensionStore name.
|
||||||
|
* @return all ExtensionStores which names start with the prefix.
|
||||||
|
*/
|
||||||
|
List<ExtensionStore> listByNamePrefix(String prefix);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches an ExtensionStore by unique name.
|
||||||
|
*
|
||||||
|
* @param name is the full name of an ExtensionStore.
|
||||||
|
* @return an optional ExtensionStore.
|
||||||
|
*/
|
||||||
|
Optional<ExtensionStore> fetchByName(String name);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an ExtensionStore.
|
||||||
|
*
|
||||||
|
* @param name is the full name of an ExtensionStore.
|
||||||
|
* @param data is Extension body to be persisted.
|
||||||
|
* @return a fresh ExtensionStore created just now.
|
||||||
|
*/
|
||||||
|
ExtensionStore create(String name, byte[] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an ExtensionStore with version to prevent concurrent update.
|
||||||
|
*
|
||||||
|
* @param name is the full name of an ExtensionStore.
|
||||||
|
* @param version is the expected version of ExtensionStore.
|
||||||
|
* @param data is Extension body to be updated.
|
||||||
|
* @return updated ExtensionStore with a fresh version.
|
||||||
|
*/
|
||||||
|
ExtensionStore update(String name, Long version, byte[] data);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an ExtensionStore by name and current version.
|
||||||
|
*
|
||||||
|
* @param name is the full name of an ExtensionStore.
|
||||||
|
* @param version is the expected version of ExtensionStore.
|
||||||
|
* @return previous ExtensionStore.
|
||||||
|
*/
|
||||||
|
ExtensionStore delete(String name, Long version);
|
||||||
|
|
||||||
|
//TODO add watch method here.
|
||||||
|
}
|
|
@ -0,0 +1,53 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of ExtensionStoreClient using JPA.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ExtensionStoreClientJPAImpl implements ExtensionStoreClient {
|
||||||
|
|
||||||
|
private final ExtensionStoreRepository repository;
|
||||||
|
|
||||||
|
public ExtensionStoreClientJPAImpl(ExtensionStoreRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ExtensionStore> listByNamePrefix(String prefix) {
|
||||||
|
return repository.findAllByNameStartingWith(prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ExtensionStore> fetchByName(String name) {
|
||||||
|
return repository.findById(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtensionStore create(String name, byte[] data) {
|
||||||
|
var store = new ExtensionStore(name, data);
|
||||||
|
return repository.save(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtensionStore update(String name, Long version, byte[] data) {
|
||||||
|
var store = new ExtensionStore(name, data, version);
|
||||||
|
return repository.save(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExtensionStore delete(String name, Long version) {
|
||||||
|
var extensionStore =
|
||||||
|
repository.findById(name).orElseThrow(EntityNotFoundException::new);
|
||||||
|
extensionStore.setVersion(version);
|
||||||
|
repository.delete(extensionStore);
|
||||||
|
return extensionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This repository contains some basic operations on ExtensionStore entity.
|
||||||
|
*
|
||||||
|
* @author johnniang
|
||||||
|
*/
|
||||||
|
@Repository
|
||||||
|
public interface ExtensionStoreRepository extends JpaRepository<ExtensionStore, String> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all ExtensionStore by name prefix.
|
||||||
|
*
|
||||||
|
* @param prefix is the prefix of name.
|
||||||
|
* @return all ExtensionStores which names starts with the given prefix.
|
||||||
|
*/
|
||||||
|
List<ExtensionStore> findAllByNameStartingWith(String prefix);
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class AbstractExtensionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void groupVersionKind() {
|
||||||
|
var extension = new AbstractExtension() {
|
||||||
|
};
|
||||||
|
extension.setApiVersion("fake.halo.run/v1alpha1");
|
||||||
|
extension.setKind("Fake");
|
||||||
|
var gvk = extension.groupVersionKind();
|
||||||
|
|
||||||
|
assertEquals(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"), gvk);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testGroupVersionKind() {
|
||||||
|
var extension = new AbstractExtension() {
|
||||||
|
};
|
||||||
|
extension.groupVersionKind(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"));
|
||||||
|
|
||||||
|
assertEquals("fake.halo.run/v1alpha1", extension.getApiVersion());
|
||||||
|
assertEquals("Fake", extension.getKind());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void metadata() {
|
||||||
|
var extension = new AbstractExtension() {
|
||||||
|
};
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName("fake");
|
||||||
|
extension.setMetadata(metadata);
|
||||||
|
|
||||||
|
assertEquals(metadata, extension.getMetadata());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testMetadata() {
|
||||||
|
var extension = new AbstractExtension() {
|
||||||
|
};
|
||||||
|
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName("fake");
|
||||||
|
extension.metadata(metadata);
|
||||||
|
|
||||||
|
assertEquals(metadata, extension.getMetadata());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
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.assertEquals;
|
||||||
|
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.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
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.data.domain.PageImpl;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import run.halo.app.extension.exception.SchemeNotFoundException;
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
import run.halo.app.extension.store.ExtensionStoreClient;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DefaultExtensionClientTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionStoreClient storeClient;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionConverter converter;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
DefaultExtensionClient client;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void before() {
|
||||||
|
Schemes.INSTANCE.register(FakeExtension.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
var extensionStore = new ExtensionStore();
|
||||||
|
extensionStore.setName(name);
|
||||||
|
return extensionStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowSchemeNotFoundExceptionWhenSchemeNotRegistered() {
|
||||||
|
class UnRegisteredExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.list(UnRegisteredExtension.class, null, null));
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.page(UnRegisteredExtension.class, null, null, 0, 10));
|
||||||
|
assertThrows(SchemeNotFoundException.class,
|
||||||
|
() -> client.fetch(UnRegisteredExtension.class, "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(emptyList());
|
||||||
|
var fakes = client.list(FakeExtension.class, null, null);
|
||||||
|
assertEquals(emptyList(), fakes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
List.of(createExtensionStore("fake-01"), createExtensionStore("fake-02")));
|
||||||
|
|
||||||
|
// without filter and sorter
|
||||||
|
var fakes = client.list(FakeExtension.class, null, null);
|
||||||
|
assertEquals(List.of(fake1, fake2), fakes);
|
||||||
|
|
||||||
|
// with filter
|
||||||
|
fakes = client.list(FakeExtension.class, fake -> {
|
||||||
|
String name = fake.getMetadata().getName();
|
||||||
|
return "fake-01".equals(name);
|
||||||
|
}, null);
|
||||||
|
assertEquals(List.of(fake1), fakes);
|
||||||
|
|
||||||
|
// with sorter
|
||||||
|
fakes = client.list(FakeExtension.class, null,
|
||||||
|
reverseOrder(comparing(fake -> fake.getMetadata().getName())));
|
||||||
|
assertEquals(List.of(fake2, fake1), fakes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(
|
||||||
|
List.of(createExtensionStore("fake-01"), createExtensionStore("fake-02"),
|
||||||
|
createExtensionStore("fake-03")));
|
||||||
|
|
||||||
|
// without filter and sorter.
|
||||||
|
var fakes = client.page(FakeExtension.class, null, null, 0, 10);
|
||||||
|
assertEquals(new PageImpl<>(List.of(fake1, fake2, fake3), PageRequest.of(0, 10), 3), fakes);
|
||||||
|
|
||||||
|
// out of page range
|
||||||
|
fakes = client.page(FakeExtension.class, null, null, 100, 10);
|
||||||
|
assertEquals(new PageImpl<>(emptyList(), PageRequest.of(100, 10), 3), fakes);
|
||||||
|
|
||||||
|
// with filter only
|
||||||
|
fakes =
|
||||||
|
client.page(FakeExtension.class, fake -> "fake-03".equals(fake.getMetadata().getName()),
|
||||||
|
null, 0, 10);
|
||||||
|
assertEquals(new PageImpl<>(List.of(fake3), PageRequest.of(0, 10), 1), fakes);
|
||||||
|
|
||||||
|
// with sorter only
|
||||||
|
fakes = client.page(FakeExtension.class, null,
|
||||||
|
reverseOrder(comparing(fake -> fake.getMetadata().getName())), 0, 10);
|
||||||
|
assertEquals(new PageImpl<>(List.of(fake3, fake2, fake1), PageRequest.of(0, 10), 3), fakes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchNothing() {
|
||||||
|
when(storeClient.fetchByName(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
Optional<FakeExtension> fake = client.fetch(FakeExtension.class, "fake");
|
||||||
|
|
||||||
|
assertEquals(Optional.empty(), fake);
|
||||||
|
verify(converter, times(0)).convertFrom(any(), any());
|
||||||
|
verify(storeClient, times(1)).fetchByName(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchAnExtension() {
|
||||||
|
var storeName = "/registry/fake.halo.run/fakes/fake";
|
||||||
|
when(storeClient.fetchByName(storeName)).thenReturn(
|
||||||
|
Optional.of(createExtensionStore(storeName)));
|
||||||
|
|
||||||
|
when(
|
||||||
|
converter.convertFrom(FakeExtension.class, createExtensionStore(storeName))).thenReturn(
|
||||||
|
createFakeExtension("fake", 1L));
|
||||||
|
|
||||||
|
Optional<FakeExtension> fake = client.fetch(FakeExtension.class, "fake");
|
||||||
|
assertEquals(Optional.of(createFakeExtension("fake", 1L)), fake);
|
||||||
|
|
||||||
|
verify(storeClient, times(1)).fetchByName(eq(storeName));
|
||||||
|
verify(converter, times(1)).convertFrom(eq(FakeExtension.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(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
|
||||||
|
client.create(fake);
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(any());
|
||||||
|
verify(storeClient, times(1)).create(any(), any());
|
||||||
|
assertNotNull(fake.getMetadata().getCreationTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateSuccessfully() {
|
||||||
|
var fake = createFakeExtension("fake", 2L);
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
when(storeClient.update(any(), any(), any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
|
||||||
|
client.update(fake);
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(any());
|
||||||
|
verify(storeClient, times(1)).update(any(), any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteSuccessfully() {
|
||||||
|
var fake = createFakeExtension("fake", 2L);
|
||||||
|
when(converter.convertTo(any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
when(storeClient.delete(any(), any())).thenReturn(
|
||||||
|
createExtensionStore("/registry/fake.halo.run/fakes/fake"));
|
||||||
|
|
||||||
|
client.delete(fake);
|
||||||
|
|
||||||
|
verify(converter, times(1)).convertTo(any());
|
||||||
|
verify(storeClient, times(1)).delete(any(), any());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class ExtensionUtilTest {
|
||||||
|
|
||||||
|
Scheme scheme;
|
||||||
|
|
||||||
|
Scheme grouplessScheme;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
scheme = new Scheme(FakeExtension.class,
|
||||||
|
new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"),
|
||||||
|
"fakes",
|
||||||
|
"fake",
|
||||||
|
null);
|
||||||
|
grouplessScheme = new Scheme(FakeExtension.class,
|
||||||
|
new GroupVersionKind("", "v1alpha1", "Fake"),
|
||||||
|
"fakes",
|
||||||
|
"fake",
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildStoreNamePrefix() {
|
||||||
|
var prefix = ExtensionUtil.buildStoreNamePrefix(scheme);
|
||||||
|
assertEquals("/registry/fake.halo.run/fakes", prefix);
|
||||||
|
|
||||||
|
prefix = ExtensionUtil.buildStoreNamePrefix(grouplessScheme);
|
||||||
|
assertEquals("/registry/fakes", prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void buildStoreName() {
|
||||||
|
var storeName = ExtensionUtil.buildStoreName(scheme, "fake-name");
|
||||||
|
assertEquals("/registry/fake.halo.run/fakes/fake-name", storeName);
|
||||||
|
|
||||||
|
storeName = ExtensionUtil.buildStoreName(grouplessScheme, "fake-name");
|
||||||
|
assertEquals("/registry/fakes/fake-name", storeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
@GVK(group = "fake.halo.run",
|
||||||
|
version = "v1alpha1",
|
||||||
|
kind = "Fake",
|
||||||
|
plural = "fakes",
|
||||||
|
singular = "fake")
|
||||||
|
class FakeExtension extends AbstractExtension {
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class GroupVersionTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowIllegalArgumentExceptionWhenAPIVersionIsIllegal() {
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(null),
|
||||||
|
"apiVersion is null");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(""),
|
||||||
|
"apiVersion is empty");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion(" "),
|
||||||
|
"apiVersion is blank");
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> GroupVersion.parseAPIVersion("a/b/c"),
|
||||||
|
"apiVersion contains more than 1 '/'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnGroupVersionCorrectly() {
|
||||||
|
assertEquals(new GroupVersion("", "v1"), GroupVersion.parseAPIVersion("v1"),
|
||||||
|
"only contains version");
|
||||||
|
assertEquals(new GroupVersion("core.halo.run", "v1"),
|
||||||
|
GroupVersion.parseAPIVersion("core.halo.run/v1"), "only contains version");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import run.halo.app.extension.exception.ExtensionConvertException;
|
||||||
|
import run.halo.app.extension.store.ExtensionStore;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class JSONExtensionConverterTest {
|
||||||
|
|
||||||
|
JSONExtensionConverter converter;
|
||||||
|
|
||||||
|
ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void beforeAll() {
|
||||||
|
Schemes.INSTANCE.register(FakeExtension.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
objectMapper = new ObjectMapper();
|
||||||
|
converter = new JSONExtensionConverter(objectMapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertTo() throws IOException {
|
||||||
|
var fake = createFakeExtension("fake", 10L);
|
||||||
|
|
||||||
|
var extensionStore = converter.convertTo(fake);
|
||||||
|
|
||||||
|
assertEquals("/registry/fake.halo.run/fakes/fake", extensionStore.getName());
|
||||||
|
assertEquals(10L, extensionStore.getVersion());
|
||||||
|
assertEquals(fake, objectMapper.readValue(extensionStore.getData(), FakeExtension.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void convertFrom() throws JsonProcessingException {
|
||||||
|
var fake = createFakeExtension("fake", 20L);
|
||||||
|
|
||||||
|
var store = new ExtensionStore();
|
||||||
|
store.setName("/registry/fake.halo.run/fakes/fake");
|
||||||
|
store.setVersion(20L);
|
||||||
|
store.setData(objectMapper.writeValueAsBytes(fake));
|
||||||
|
|
||||||
|
FakeExtension gotFake = converter.convertFrom(FakeExtension.class, store);
|
||||||
|
assertEquals(fake, gotFake);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWhenDataIsInvalid() {
|
||||||
|
var store = new ExtensionStore();
|
||||||
|
store.setName("/registry/fake.halo.run/fakes/fake");
|
||||||
|
store.setVersion(20L);
|
||||||
|
store.setData("{".getBytes());
|
||||||
|
|
||||||
|
assertThrows(ExtensionConvertException.class,
|
||||||
|
() -> converter.convertFrom(FakeExtension.class, store));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FakeExtension createFakeExtension(String name, Long version) {
|
||||||
|
var fake = new FakeExtension();
|
||||||
|
fake.groupVersionKind(new GroupVersionKind("fake.halo.run", "v1alpha1", "Fake"));
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName(name);
|
||||||
|
metadata.setVersion(version);
|
||||||
|
fake.metadata(metadata);
|
||||||
|
|
||||||
|
return fake;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class SchemeTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void requiredFieldTest() {
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Scheme(null, new GroupVersionKind("", "v1alpha1", ""), "", "", null));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Scheme(FakeExtension.class, new GroupVersionKind("", "", ""), "", "",
|
||||||
|
null));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", ""), "",
|
||||||
|
"", null));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "",
|
||||||
|
"", null));
|
||||||
|
assertThrows(IllegalArgumentException.class,
|
||||||
|
() -> new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"),
|
||||||
|
"fakes", "", null));
|
||||||
|
|
||||||
|
new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "fakes",
|
||||||
|
"fake", null);
|
||||||
|
new Scheme(FakeExtension.class, new GroupVersionKind("", "v1alpha1", "Fake"), "fakes",
|
||||||
|
"fake", new ObjectNode(null));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,47 @@
|
||||||
|
package run.halo.app.extension;
|
||||||
|
|
||||||
|
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 java.util.Optional;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import run.halo.app.extension.exception.ExtensionException;
|
||||||
|
|
||||||
|
class SchemesTest {
|
||||||
|
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUp() {
|
||||||
|
Schemes.INSTANCE.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testRegister() {
|
||||||
|
Schemes.INSTANCE.register(FakeExtension.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowExceptionWithoutGVKAnnotation() {
|
||||||
|
class WithoutGVKExtension extends AbstractExtension {
|
||||||
|
}
|
||||||
|
|
||||||
|
assertThrows(ExtensionException.class,
|
||||||
|
() -> Schemes.INSTANCE.register(WithoutGVKExtension.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchNothingWhenUnregistered() {
|
||||||
|
var scheme = Schemes.INSTANCE.fetch(FakeExtension.class);
|
||||||
|
assertEquals(Optional.empty(), scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldFetchFakeWhenRegistered() {
|
||||||
|
Schemes.INSTANCE.register(FakeExtension.class);
|
||||||
|
|
||||||
|
var scheme = Schemes.INSTANCE.fetch(FakeExtension.class);
|
||||||
|
assertTrue(scheme.isPresent());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
package run.halo.app.extension.store;
|
||||||
|
|
||||||
|
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.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.doNothing;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import jakarta.persistence.EntityNotFoundException;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class ExtensionStoreClientJPAImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
ExtensionStoreRepository repository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
ExtensionStoreClientJPAImpl client;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void listByNamePrefix() {
|
||||||
|
var expectedExtensions = List.of(
|
||||||
|
new ExtensionStore("/registry/posts/hello-world", "this is post".getBytes(), 1L),
|
||||||
|
new ExtensionStore("/registry/posts/hello-halo", "this is post".getBytes(), 1L)
|
||||||
|
);
|
||||||
|
|
||||||
|
when(repository.findAllByNameStartingWith("/registry/posts"))
|
||||||
|
.thenReturn(expectedExtensions);
|
||||||
|
|
||||||
|
var gotExtensions = client.listByNamePrefix("/registry/posts");
|
||||||
|
assertEquals(expectedExtensions, gotExtensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fetchByName() {
|
||||||
|
var expectedExtension =
|
||||||
|
new ExtensionStore("/registry/posts/hello-world", "this is post".getBytes(), 1L);
|
||||||
|
|
||||||
|
when(repository.findById("/registry/posts/hello-halo")).thenReturn(
|
||||||
|
Optional.of(expectedExtension));
|
||||||
|
|
||||||
|
var gotExtension = client.fetchByName("/registry/posts/hello-halo");
|
||||||
|
assertTrue(gotExtension.isPresent());
|
||||||
|
assertEquals(expectedExtension, gotExtension.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void create() {
|
||||||
|
var expectedExtension =
|
||||||
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
|
when(repository.save(any())).thenReturn(expectedExtension);
|
||||||
|
|
||||||
|
var createdExtension =
|
||||||
|
client.create("/registry/posts/hello-halo", "hello halo".getBytes());
|
||||||
|
|
||||||
|
assertEquals(expectedExtension, createdExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void update() {
|
||||||
|
var expectedExtension =
|
||||||
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
|
when(repository.save(any())).thenReturn(expectedExtension);
|
||||||
|
|
||||||
|
var updatedExtension =
|
||||||
|
client.update("/registry/posts/hello-halo", 1L, "hello halo".getBytes());
|
||||||
|
|
||||||
|
assertEquals(expectedExtension, updatedExtension);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldThrowEntityNotFoundExceptionWhenDeletingNonExistExt() {
|
||||||
|
|
||||||
|
when(repository.findById(any())).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
assertThrows(EntityNotFoundException.class,
|
||||||
|
() -> client.delete("/registry/posts/hello-halo", 1L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDeleteSuccessfully() {
|
||||||
|
var expectedExtension =
|
||||||
|
new ExtensionStore("/registry/posts/hello-halo", "hello halo".getBytes(), 2L);
|
||||||
|
|
||||||
|
when(repository.findById(any())).thenReturn(Optional.of(expectedExtension));
|
||||||
|
doNothing().when(repository).delete(any());
|
||||||
|
|
||||||
|
var deletedExtension = client.delete("/registry/posts/hello-halo", 2L);
|
||||||
|
|
||||||
|
assertEquals(expectedExtension, deletedExtension);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue