mirror of https://github.com/halo-dev/halo
feat: count the number of posts under categories and tags (#2402)
#### What type of PR is this? /kind feature /milestone 2.0 /area core #### What this PR does / why we need it: 统计分类和标签下的文章 #### Which issue(s) this PR fixes: Fixes #2401 #### Special notes for your reviewer: how to test it? 1. 创建一个多层级的分类及若干文章,查看分类的 status.posts 是否包含当前及其子分类下的文章 2. 创建标签,并将其分配给若干文章,查看标签的 status.posts 是否正确 3. 在主题端查看分类和标签包含的文章数量是否正确,需要注意的是主题端显示的文章数量只包含已发布且 visiable 为 public 且未删除的 /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note None ```pull/2412/head^2
parent
a0d55c58f6
commit
510f155e05
|
@ -27,6 +27,11 @@ public class Category extends AbstractExtension {
|
||||||
@Schema
|
@Schema
|
||||||
private CategoryStatus status;
|
private CategoryStatus status;
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isDeleted() {
|
||||||
|
return getMetadata().getDeletionTimestamp() != null;
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class CategorySpec {
|
public static class CategorySpec {
|
||||||
|
|
||||||
|
@ -64,6 +69,6 @@ public class Category extends AbstractExtension {
|
||||||
/**
|
/**
|
||||||
* 包括当前和其下所有层级的文章 name (depth=max).
|
* 包括当前和其下所有层级的文章 name (depth=max).
|
||||||
*/
|
*/
|
||||||
private List<String> posts;
|
private List<Post.CompactPost> posts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -48,6 +49,17 @@ public class Post extends AbstractExtension {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isDeleted() {
|
||||||
|
return Objects.equals(true, spec.getDeleted())
|
||||||
|
|| getMetadata().getDeletionTimestamp() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
public boolean isPublished() {
|
||||||
|
return Objects.equals(true, spec.getPublished());
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class PostSpec {
|
public static class PostSpec {
|
||||||
@Schema(required = true, minLength = 1)
|
@Schema(required = true, minLength = 1)
|
||||||
|
@ -149,4 +161,59 @@ public class Post extends AbstractExtension {
|
||||||
INTERNAL,
|
INTERNAL,
|
||||||
PRIVATE
|
PRIVATE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class CompactPost {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private VisibleEnum visible;
|
||||||
|
|
||||||
|
private Boolean published;
|
||||||
|
|
||||||
|
public static Builder builder() {
|
||||||
|
return new Builder();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>Compact post builder.</p>
|
||||||
|
* <p>Can not replace with lombok builder.</p>
|
||||||
|
* <p>The class used by subclasses of {@link AbstractExtension} must have a no-args
|
||||||
|
* constructor.</p>
|
||||||
|
*/
|
||||||
|
public static class Builder {
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
private VisibleEnum visible;
|
||||||
|
|
||||||
|
private Boolean published;
|
||||||
|
|
||||||
|
public Builder name(String name) {
|
||||||
|
this.name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder visible(VisibleEnum visible) {
|
||||||
|
this.visible = visible;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder published(Boolean published) {
|
||||||
|
this.published = published;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build compact post.
|
||||||
|
*
|
||||||
|
* @return a compact post
|
||||||
|
*/
|
||||||
|
public CompactPost build() {
|
||||||
|
CompactPost compactPost = new CompactPost();
|
||||||
|
compactPost.setName(name);
|
||||||
|
compactPost.setVisible(visible);
|
||||||
|
compactPost.setPublished(published);
|
||||||
|
return compactPost;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,6 +69,6 @@ public class Tag extends AbstractExtension {
|
||||||
|
|
||||||
private String permalink;
|
private String permalink;
|
||||||
|
|
||||||
private List<String> posts;
|
private List<Post.CompactPost> posts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,19 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||||
import run.halo.app.core.extension.Category;
|
import run.halo.app.core.extension.Category;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.infra.utils.JsonUtils;
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
@ -27,17 +37,20 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result reconcile(Request request) {
|
public Result reconcile(Request request) {
|
||||||
client.fetch(Category.class, request.name())
|
return client.fetch(Category.class, request.name())
|
||||||
.ifPresent(category -> {
|
.map(category -> {
|
||||||
if (isDeleted(category)) {
|
if (category.isDeleted()) {
|
||||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||||
return;
|
return new Result(false, null);
|
||||||
}
|
}
|
||||||
addFinalizerIfNecessary(category);
|
addFinalizerIfNecessary(category);
|
||||||
|
|
||||||
reconcileStatus(request.name());
|
reconcileStatusPermalink(request.name());
|
||||||
});
|
|
||||||
return new Result(false, null);
|
reconcileStatusPosts(request.name());
|
||||||
|
return new Result(true, Duration.ofMinutes(1));
|
||||||
|
})
|
||||||
|
.orElseGet(() -> new Result(false, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addFinalizerIfNecessary(Category oldCategory) {
|
private void addFinalizerIfNecessary(Category oldCategory) {
|
||||||
|
@ -72,7 +85,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reconcileStatus(String name) {
|
private void reconcileStatusPermalink(String name) {
|
||||||
client.fetch(Category.class, name)
|
client.fetch(Category.class, name)
|
||||||
.ifPresent(category -> {
|
.ifPresent(category -> {
|
||||||
Category oldCategory = JsonUtils.deepCopy(category);
|
Category oldCategory = JsonUtils.deepCopy(category);
|
||||||
|
@ -88,7 +101,79 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDeleted(Category category) {
|
private void reconcileStatusPosts(String name) {
|
||||||
return category.getMetadata().getDeletionTimestamp() != null;
|
client.fetch(Category.class, name).ifPresent(category -> {
|
||||||
|
Category oldCategory = JsonUtils.deepCopy(category);
|
||||||
|
|
||||||
|
populatePosts(category);
|
||||||
|
|
||||||
|
if (!oldCategory.equals(category)) {
|
||||||
|
client.update(category);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populatePosts(Category category) {
|
||||||
|
String name = category.getMetadata().getName();
|
||||||
|
List<String> categoryNames = listChildrenByName(name)
|
||||||
|
.stream()
|
||||||
|
.map(item -> item.getMetadata().getName())
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
List<Post> posts = client.list(Post.class, post -> !post.isDeleted(), null);
|
||||||
|
|
||||||
|
// populate post to status
|
||||||
|
List<Post.CompactPost> compactPosts = posts.stream()
|
||||||
|
.filter(post -> includes(post.getSpec().getCategories(), categoryNames))
|
||||||
|
.map(post -> Post.CompactPost.builder()
|
||||||
|
.name(post.getMetadata().getName())
|
||||||
|
.visible(post.getSpec().getVisible())
|
||||||
|
.published(post.isPublished())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
category.getStatusOrDefault().setPosts(compactPosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* whether {@code categoryRefs} contains elements in {@code categoryNames}.
|
||||||
|
*
|
||||||
|
* @param categoryRefs category left to judge
|
||||||
|
* @param categoryNames category right to judge
|
||||||
|
* @return true if {@code categoryRefs} contains elements in {@code categoryNames}.
|
||||||
|
*/
|
||||||
|
private boolean includes(@Nullable List<String> categoryRefs, List<String> categoryNames) {
|
||||||
|
if (categoryRefs == null || categoryNames == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (String categoryRef : categoryRefs) {
|
||||||
|
if (categoryNames.contains(categoryRef)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Category> listChildrenByName(String name) {
|
||||||
|
List<Category> categories = client.list(Category.class, null, null);
|
||||||
|
Map<String, Category> nameIdentityMap = categories.stream()
|
||||||
|
.collect(Collectors.toMap(category -> category.getMetadata().getName(),
|
||||||
|
Function.identity()));
|
||||||
|
final List<Category> children = new ArrayList<>();
|
||||||
|
|
||||||
|
Deque<String> deque = new ArrayDeque<>();
|
||||||
|
deque.add(name);
|
||||||
|
while (!deque.isEmpty()) {
|
||||||
|
String itemName = deque.poll();
|
||||||
|
Category category = nameIdentityMap.get(itemName);
|
||||||
|
if (category == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
children.add(category);
|
||||||
|
List<String> childrenNames = category.getSpec().getChildren();
|
||||||
|
if (childrenNames != null) {
|
||||||
|
deque.addAll(childrenNames);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return children;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.core.extension.Tag;
|
import run.halo.app.core.extension.Tag;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
@ -26,17 +29,20 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result reconcile(Request request) {
|
public Result reconcile(Request request) {
|
||||||
client.fetch(Tag.class, request.name())
|
return client.fetch(Tag.class, request.name())
|
||||||
.ifPresent(tag -> {
|
.map(tag -> {
|
||||||
if (isDeleted(tag)) {
|
if (isDeleted(tag)) {
|
||||||
cleanUpResourcesAndRemoveFinalizer(request.name());
|
cleanUpResourcesAndRemoveFinalizer(request.name());
|
||||||
return;
|
return new Result(false, null);
|
||||||
}
|
}
|
||||||
addFinalizerIfNecessary(tag);
|
addFinalizerIfNecessary(tag);
|
||||||
|
|
||||||
this.reconcileStatus(request.name());
|
this.reconcileStatusPermalink(request.name());
|
||||||
});
|
|
||||||
return new Result(false, null);
|
reconcileStatusPosts(request.name());
|
||||||
|
return new Result(true, Duration.ofMinutes(1));
|
||||||
|
})
|
||||||
|
.orElseGet(() -> new Result(false, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void cleanUpResources(Tag tag) {
|
private void cleanUpResources(Tag tag) {
|
||||||
|
@ -71,7 +77,7 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void reconcileStatus(String tagName) {
|
private void reconcileStatusPermalink(String tagName) {
|
||||||
client.fetch(Tag.class, tagName)
|
client.fetch(Tag.class, tagName)
|
||||||
.ifPresent(tag -> {
|
.ifPresent(tag -> {
|
||||||
Tag oldTag = JsonUtils.deepCopy(tag);
|
Tag oldTag = JsonUtils.deepCopy(tag);
|
||||||
|
@ -87,6 +93,38 @@ public class TagReconciler implements Reconciler<Reconciler.Request> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void reconcileStatusPosts(String tagName) {
|
||||||
|
client.fetch(Tag.class, tagName).ifPresent(tag -> {
|
||||||
|
Tag oldTag = JsonUtils.deepCopy(tag);
|
||||||
|
|
||||||
|
populatePosts(tag);
|
||||||
|
|
||||||
|
if (!oldTag.equals(tag)) {
|
||||||
|
client.update(tag);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populatePosts(Tag tag) {
|
||||||
|
List<Post.CompactPost> compactPosts = client.list(Post.class, null, null)
|
||||||
|
.stream()
|
||||||
|
.filter(post -> includes(post.getSpec().getTags(), tag.getMetadata().getName()))
|
||||||
|
.map(post -> Post.CompactPost.builder()
|
||||||
|
.name(post.getMetadata().getName())
|
||||||
|
.published(post.isPublished())
|
||||||
|
.visible(post.getSpec().getVisible())
|
||||||
|
.build())
|
||||||
|
.toList();
|
||||||
|
tag.getStatusOrDefault().setPosts(compactPosts);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean includes(List<String> tags, String tagName) {
|
||||||
|
if (tags == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return tags.contains(tagName);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isDeleted(Tag tag) {
|
private boolean isDeleted(Tag tag) {
|
||||||
return tag.getMetadata().getDeletionTimestamp() != null;
|
return tag.getMetadata().getDeletionTimestamp() != null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
package run.halo.app.theme.finders.vo;
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import run.halo.app.core.extension.Category;
|
import run.halo.app.core.extension.Category;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,4 +48,19 @@ public class CategoryTreeVo {
|
||||||
.children(List.of())
|
.children(List.of())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of posts under the current category and its sub categories.
|
||||||
|
*
|
||||||
|
* @return the number of posts
|
||||||
|
*/
|
||||||
|
public long postCount() {
|
||||||
|
if (this.status == null || this.status.getPosts() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.status.getPosts().stream()
|
||||||
|
.filter(post -> Objects.equals(true, post.getPublished())
|
||||||
|
&& Post.VisibleEnum.PUBLIC.equals(post.getVisible()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package run.halo.app.theme.finders.vo;
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
import run.halo.app.core.extension.Category;
|
import run.halo.app.core.extension.Category;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,4 +38,19 @@ public class CategoryVo {
|
||||||
.status(category.getStatus())
|
.status(category.getStatus())
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of posts under the current category and its sub categories.
|
||||||
|
*
|
||||||
|
* @return the number of posts
|
||||||
|
*/
|
||||||
|
public long postCount() {
|
||||||
|
if (this.status == null || this.status.getPosts() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.status.getPosts().stream()
|
||||||
|
.filter(post -> Objects.equals(true, post.getPublished())
|
||||||
|
&& Post.VisibleEnum.PUBLIC.equals(post.getVisible()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package run.halo.app.theme.finders.vo;
|
package run.halo.app.theme.finders.vo;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Value;
|
import lombok.Value;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.core.extension.Tag;
|
import run.halo.app.core.extension.Tag;
|
||||||
import run.halo.app.extension.MetadataOperator;
|
import run.halo.app.extension.MetadataOperator;
|
||||||
|
|
||||||
|
@ -33,4 +35,20 @@ public class TagVo {
|
||||||
.status(status)
|
.status(status)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of posts under the current tag.
|
||||||
|
*
|
||||||
|
* @return the number of posts
|
||||||
|
*/
|
||||||
|
public long postCount() {
|
||||||
|
if (this.status == null || this.status.getPosts() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.status.getPosts()
|
||||||
|
.stream()
|
||||||
|
.filter(post -> Objects.equals(true, post.getPublished())
|
||||||
|
&& Post.VisibleEnum.PUBLIC.equals(post.getVisible()))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,224 @@
|
||||||
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import run.halo.app.content.TestPost;
|
||||||
|
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||||
|
import run.halo.app.core.extension.Category;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link CategoryReconciler}.
|
||||||
|
*
|
||||||
|
* @author guqing
|
||||||
|
* @since 2.0.0
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class CategoryReconcilerTest {
|
||||||
|
@Mock
|
||||||
|
private ExtensionClient client;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private CategoryPermalinkPolicy categoryPermalinkPolicy;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private CategoryReconciler categoryReconciler;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileStatusPostForCategoryA() throws JSONException {
|
||||||
|
reconcileStatusPostPilling("category-A");
|
||||||
|
|
||||||
|
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post-1",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-2",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-3",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-4",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(captor.getAllValues().get(1).getStatusOrDefault().getPosts()),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileStatusPostForCategoryB() throws JSONException {
|
||||||
|
reconcileStatusPostPilling("category-B");
|
||||||
|
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post-1",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-2",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-3",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(captor.getAllValues().get(1).getStatusOrDefault().getPosts()),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileStatusPostForCategoryC() throws JSONException {
|
||||||
|
reconcileStatusPostPilling("category-C");
|
||||||
|
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post-1",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "post-2",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(captor.getAllValues().get(1).getStatusOrDefault().getPosts()),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileStatusPostForCategoryD() throws JSONException {
|
||||||
|
reconcileStatusPostPilling("category-D");
|
||||||
|
ArgumentCaptor<Category> captor = ArgumentCaptor.forClass(Category.class);
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "post-1",
|
||||||
|
"visible": "PUBLIC",
|
||||||
|
"published": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(captor.getAllValues().get(1).getStatusOrDefault().getPosts()),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void reconcileStatusPostPilling(String reconcileCategoryName) {
|
||||||
|
categories().forEach(category -> {
|
||||||
|
lenient().when(client.fetch(eq(Category.class), eq(category.getMetadata().getName())))
|
||||||
|
.thenReturn(Optional.of(category));
|
||||||
|
});
|
||||||
|
|
||||||
|
lenient().when(client.list(eq(Post.class), any(), any()))
|
||||||
|
.thenReturn(posts());
|
||||||
|
lenient().when(client.list(eq(Category.class), any(), any()))
|
||||||
|
.thenReturn(categories());
|
||||||
|
|
||||||
|
Reconciler.Result result =
|
||||||
|
categoryReconciler.reconcile(new Reconciler.Request(reconcileCategoryName));
|
||||||
|
assertThat(result.reEnqueue()).isTrue();
|
||||||
|
assertThat(result.retryAfter()).isEqualTo(Duration.ofMinutes(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Category> categories() {
|
||||||
|
/*
|
||||||
|
* |-A(post-4)
|
||||||
|
* |-B(post-3)
|
||||||
|
* |-|-C(post-2,post-1)
|
||||||
|
* |-D(post-1)
|
||||||
|
*/
|
||||||
|
Category categoryA = category("category-A");
|
||||||
|
categoryA.getSpec().setChildren(List.of("category-B", "category-D"));
|
||||||
|
|
||||||
|
Category categoryB = category("category-B");
|
||||||
|
categoryB.getSpec().setChildren(List.of("category-C"));
|
||||||
|
|
||||||
|
Category categoryC = category("category-C");
|
||||||
|
Category categoryD = category("category-D");
|
||||||
|
return List.of(categoryA, categoryB, categoryC, categoryD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Category category(String name) {
|
||||||
|
Category category = new Category();
|
||||||
|
Metadata metadata = new Metadata();
|
||||||
|
metadata.setName(name);
|
||||||
|
category.setMetadata(metadata);
|
||||||
|
category.setSpec(new Category.CategorySpec());
|
||||||
|
category.setStatus(new Category.CategoryStatus());
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Post> posts() {
|
||||||
|
/*
|
||||||
|
* |-A(post-4)
|
||||||
|
* |-B(post-3)
|
||||||
|
* |-|-C(post-2,post-1)
|
||||||
|
* |-D(post-1)
|
||||||
|
*/
|
||||||
|
Post post1 = TestPost.postV1();
|
||||||
|
post1.getMetadata().setName("post-1");
|
||||||
|
post1.getSpec().setCategories(List.of("category-D", "category-C"));
|
||||||
|
post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||||
|
|
||||||
|
Post post2 = TestPost.postV1();
|
||||||
|
post2.getMetadata().setName("post-2");
|
||||||
|
post2.getSpec().setCategories(List.of("category-C"));
|
||||||
|
post2.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||||
|
|
||||||
|
Post post3 = TestPost.postV1();
|
||||||
|
post3.getMetadata().setName("post-3");
|
||||||
|
post3.getSpec().setCategories(List.of("category-B"));
|
||||||
|
post3.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||||
|
|
||||||
|
Post post4 = TestPost.postV1();
|
||||||
|
post4.getMetadata().setName("post-4");
|
||||||
|
post4.getSpec().setCategories(List.of("category-A"));
|
||||||
|
post4.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||||
|
return List.of(post1, post2, post3, post4);
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,17 +8,23 @@ import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import org.json.JSONException;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import run.halo.app.content.TestPost;
|
||||||
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
import run.halo.app.content.permalinks.TagPermalinkPolicy;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
import run.halo.app.core.extension.Tag;
|
import run.halo.app.core.extension.Tag;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.infra.utils.JsonUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tests for {@link TagReconciler}.
|
* Tests for {@link TagReconciler}.
|
||||||
|
@ -48,7 +54,7 @@ class TagReconcilerTest {
|
||||||
|
|
||||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||||
|
|
||||||
verify(client, times(2)).update(captor.capture());
|
verify(client, times(3)).update(captor.capture());
|
||||||
verify(tagPermalinkPolicy, times(1)).onPermalinkAdd(any());
|
verify(tagPermalinkPolicy, times(1)).onPermalinkAdd(any());
|
||||||
verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any());
|
verify(tagPermalinkPolicy, times(1)).onPermalinkDelete(any());
|
||||||
Tag capture = captor.getValue();
|
Tag capture = captor.getValue();
|
||||||
|
@ -57,7 +63,7 @@ class TagReconcilerTest {
|
||||||
// change slug
|
// change slug
|
||||||
tag.getSpec().setSlug("new-slug");
|
tag.getSpec().setSlug("new-slug");
|
||||||
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||||
verify(client, times(3)).update(captor.capture());
|
verify(client, times(4)).update(captor.capture());
|
||||||
verify(tagPermalinkPolicy, times(2)).onPermalinkAdd(any());
|
verify(tagPermalinkPolicy, times(2)).onPermalinkAdd(any());
|
||||||
verify(tagPermalinkPolicy, times(2)).onPermalinkDelete(any());
|
verify(tagPermalinkPolicy, times(2)).onPermalinkDelete(any());
|
||||||
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/new-slug");
|
assertThat(capture.getStatus().getPermalink()).isEqualTo("/tags/new-slug");
|
||||||
|
@ -78,6 +84,33 @@ class TagReconcilerTest {
|
||||||
verify(tagPermalinkPolicy, times(0)).permalink(any());
|
verify(tagPermalinkPolicy, times(0)).permalink(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reconcileStatusPosts() throws JSONException {
|
||||||
|
Tag tag = tag();
|
||||||
|
when(client.fetch(eq(Tag.class), eq("fake-tag")))
|
||||||
|
.thenReturn(Optional.of(tag));
|
||||||
|
when(client.list(eq(Post.class), any(), any())).thenReturn(posts());
|
||||||
|
|
||||||
|
ArgumentCaptor<Tag> captor = ArgumentCaptor.forClass(Tag.class);
|
||||||
|
tagReconciler.reconcile(new TagReconciler.Request("fake-tag"));
|
||||||
|
verify(client, times(2)).update(captor.capture());
|
||||||
|
List<Tag> allValues = captor.getAllValues();
|
||||||
|
List<Post.CompactPost> posts = allValues.get(1).getStatusOrDefault().getPosts();
|
||||||
|
JSONAssert.assertEquals("""
|
||||||
|
[{
|
||||||
|
"name": "fake-post-1",
|
||||||
|
"published": false,
|
||||||
|
"visible": "PUBLIC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "fake-post-3",
|
||||||
|
"published": false,
|
||||||
|
"visible": "PRIVATE"
|
||||||
|
}]
|
||||||
|
""",
|
||||||
|
JsonUtils.objectToJson(posts), true);
|
||||||
|
}
|
||||||
|
|
||||||
Tag tag() {
|
Tag tag() {
|
||||||
Tag tag = new Tag();
|
Tag tag = new Tag();
|
||||||
tag.setMetadata(new Metadata());
|
tag.setMetadata(new Metadata());
|
||||||
|
@ -89,4 +122,23 @@ class TagReconcilerTest {
|
||||||
tag.setStatus(new Tag.TagStatus());
|
tag.setStatus(new Tag.TagStatus());
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<Post> posts() {
|
||||||
|
Post post1 = TestPost.postV1();
|
||||||
|
post1.getMetadata().setName("fake-post-1");
|
||||||
|
post1.getSpec().setVisible(Post.VisibleEnum.PUBLIC);
|
||||||
|
post1.getSpec().setTags(List.of("fake-tag", "tag-A", "tag-B"));
|
||||||
|
|
||||||
|
Post post2 = TestPost.postV1();
|
||||||
|
post2.getMetadata().setName("fake-post-2");
|
||||||
|
post2.getSpec().setVisible(Post.VisibleEnum.INTERNAL);
|
||||||
|
post2.getSpec().setTags(List.of("tag-A", "tag-C"));
|
||||||
|
|
||||||
|
Post post3 = TestPost.postV1();
|
||||||
|
post3.getMetadata().setName("fake-post-3");
|
||||||
|
post3.getSpec().setVisible(Post.VisibleEnum.PRIVATE);
|
||||||
|
post3.getSpec().setTags(List.of("tag-A", "fake-tag"));
|
||||||
|
return List.of(post1, post2, post3);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -65,10 +65,7 @@ class TagFinderImplTest {
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"permalink": "permalink-1",
|
"permalink": "permalink-1",
|
||||||
"posts": [
|
"posts": []
|
||||||
"p1",
|
|
||||||
"p2"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""",
|
""",
|
||||||
|
@ -120,7 +117,7 @@ class TagFinderImplTest {
|
||||||
|
|
||||||
Tag.TagStatus tagStatus = new Tag.TagStatus();
|
Tag.TagStatus tagStatus = new Tag.TagStatus();
|
||||||
tagStatus.setPermalink("permalink-" + i);
|
tagStatus.setPermalink("permalink-" + i);
|
||||||
tagStatus.setPosts(List.of("p1", "p2"));
|
tagStatus.setPosts(List.of());
|
||||||
tag.setStatus(tagStatus);
|
tag.setStatus(tagStatus);
|
||||||
return tag;
|
return tag;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue