feat: support custom rendering templates configure through themes (#2638)

#### What type of PR is this?
/kind feature
/milestone 2.0
/area core
#### What this PR does / why we need it:
文章/自定义页面/分类支持通过主题配置多套渲染模板
首先需要在 theme.yaml 中声明模板,以文章为例:
```yaml
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
  name: fake-theme-name
spec:
  # ...
  customTemplates:
    # 支持通过以下形式配置 post, category, page 
    post:
      - name: 新闻
        description: 新闻类型的文章模板
        screenshot: foo.png
        # file 路径相对于主题目录的 templates 目录,.html 后缀可以带也可以省略
        # 默认 thymeleaf 查找 html 文件,除非修改了 thymeleaf suffix 配置
        file: post_news.html
```
当文章页选择模板时便可得到下拉列表以配置使用哪个模板 see https://github.com/halo-dev/halo/issues/2322#issuecomment-1215135195
选择了模板之后将模板名存储到 post 的 spec.template 中,渲染时便会首先使用 spec.template,如果该模板不存在则渲染默认模板
#### Which issue(s) this PR fixes:

Fixes #2569

#### Special notes for your reviewer:
how to test it?
1. 创建一篇文章并发布
2. 安装一个主题并查看文章详情,默认渲染主题提供的 post.html 页面
3. 修改此文章的 spec.template 字段为一个新的 html 例如 spec.template=post_docs
4. 查看该文章的详情页会渲染为 post_docs.html
5. 分类、自定义页面亦如是

/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
文章/自定义页面/分类支持通过主题配置多套渲染模板
```
pull/2663/head
guqing 2022-11-02 14:22:17 +08:00 committed by GitHub
parent c0986a4b4c
commit 1078145b18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 322 additions and 30 deletions

View File

@ -1,6 +1,7 @@
package run.halo.app.core.extension; package run.halo.app.core.extension;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
import lombok.ToString; import lombok.ToString;
@ -10,6 +11,8 @@ import run.halo.app.extension.AbstractExtension;
import run.halo.app.extension.GVK; import run.halo.app.extension.GVK;
/** /**
* <p>Theme extension.</p>
*
* @author guqing * @author guqing
* @since 2.0.0 * @since 2.0.0
*/ */
@ -54,6 +57,9 @@ public class Theme extends AbstractExtension {
private String configMapName; private String configMapName;
@Schema
private CustomTemplates customTemplates;
@NonNull @NonNull
public String getVersion() { public String getVersion() {
if (StringUtils.isBlank(this.version)) { if (StringUtils.isBlank(this.version)) {
@ -85,4 +91,33 @@ public class Theme extends AbstractExtension {
private String website; private String website;
} }
@Data
public static class CustomTemplates {
private List<TemplateDescriptor> post;
private List<TemplateDescriptor> category;
private List<TemplateDescriptor> page;
}
/**
* Type used to describe custom template page.
*
* @author guqing
* @since 2.0.0
*/
@Data
public static class TemplateDescriptor {
@Schema(required = true, minLength = 1)
private String name;
private String description;
private String screenshot;
@Schema(required = true, minLength = 1)
private String file;
}
} }

View File

@ -0,0 +1,48 @@
package run.halo.app.theme.router;
import java.util.Locale;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Mono;
import run.halo.app.theme.HaloViewResolver;
/**
* The {@link ViewNameResolver} is used to resolve view name.
*
* @author guqing
* @since 2.0.0
*/
@Component
@AllArgsConstructor
public class ViewNameResolver {
private final HaloViewResolver haloViewResolver;
private final ThymeleafProperties thymeleafProperties;
/**
* Resolves view name.
* If the {@param #name} cannot be resolved to the view, the {@param #defaultName} is returned.
*/
public Mono<String> resolveViewNameOrDefault(ServerRequest request, String name,
String defaultName) {
if (StringUtils.isBlank(name)) {
return Mono.just(defaultName);
}
final String nameToUse = processName(name);
Locale locale = LocaleContextHolder.getLocale(request.exchange().getLocaleContext());
return haloViewResolver.resolveViewName(nameToUse, locale)
.map(view -> nameToUse)
.switchIfEmpty(Mono.just(defaultName));
}
String processName(String name) {
String nameToLookup = name;
if (StringUtils.endsWith(name, thymeleafProperties.getSuffix())) {
nameToLookup = StringUtils.substringBeforeLast(name, thymeleafProperties.getSuffix());
}
return nameToLookup;
}
}

View File

@ -3,6 +3,7 @@ package run.halo.app.theme.router.strategy;
import static run.halo.app.theme.router.PageUrlUtils.pageNum; import static run.halo.app.theme.router.PageUrlUtils.pageNum;
import static run.halo.app.theme.router.PageUrlUtils.totalPage; import static run.halo.app.theme.router.PageUrlUtils.totalPage;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.HandlerFunction;
@ -20,6 +21,7 @@ import run.halo.app.theme.finders.vo.CategoryVo;
import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.PageUrlUtils; import run.halo.app.theme.router.PageUrlUtils;
import run.halo.app.theme.router.UrlContextListResult; import run.halo.app.theme.router.UrlContextListResult;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template * The {@link CategoryRouteStrategy} for generate {@link HandlerFunction} specific to the template
@ -35,10 +37,13 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
private final CategoryFinder categoryFinder; private final CategoryFinder categoryFinder;
private final ViewNameResolver viewNameResolver;
public CategoryRouteStrategy(PostFinder postFinder, public CategoryRouteStrategy(PostFinder postFinder,
CategoryFinder categoryFinder) { CategoryFinder categoryFinder, ViewNameResolver viewNameResolver) {
this.postFinder = postFinder; this.postFinder = postFinder;
this.categoryFinder = categoryFinder; this.categoryFinder = categoryFinder;
this.viewNameResolver = viewNameResolver;
} }
private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name, private Mono<UrlContextListResult<PostVo>> postListByCategoryName(String name,
@ -54,19 +59,27 @@ public class CategoryRouteStrategy implements DetailsPageRouteHandlerStrategy {
} }
private Mono<CategoryVo> categoryByName(String name) { private Mono<CategoryVo> categoryByName(String name) {
return Mono.defer(() -> Mono.just(categoryFinder.getByName(name))) return Mono.fromCallable(() -> categoryFinder.getByName(name))
.publishOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
} }
@Override @Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules, public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) { String name) {
return request -> ServerResponse.ok() return request -> {
.render(DefaultTemplateEnum.CATEGORY.getValue(), Map<String, Object> model = new HashMap<>();
Map.of("name", name, model.put("name", name);
"posts", postListByCategoryName(name, request), model.put("posts", postListByCategoryName(name, request));
"category", categoryByName(name),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue())); model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.CATEGORY.getValue());
return categoryByName(name).flatMap(categoryVo -> {
model.put("category", categoryVo);
String template = categoryVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,
DefaultTemplateEnum.CATEGORY.getValue())
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
});
};
} }
@Override @Override

View File

@ -17,6 +17,7 @@ import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template * The {@link PostRouteStrategy} for generate {@link HandlerFunction} specific to the template
@ -30,9 +31,11 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
static final String NAME_PARAM = "name"; static final String NAME_PARAM = "name";
private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class); private final GroupVersionKind groupVersionKind = GroupVersionKind.fromExtension(Post.class);
private final PostFinder postFinder; private final PostFinder postFinder;
private final ViewNameResolver viewNameResolver;
public PostRouteStrategy(PostFinder postFinder) { public PostRouteStrategy(PostFinder postFinder, ViewNameResolver viewNameResolver) {
this.postFinder = postFinder; this.postFinder = postFinder;
this.viewNameResolver = viewNameResolver;
} }
@Override @Override
@ -49,15 +52,19 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
if (pathMatchInfo != null) { if (pathMatchInfo != null) {
model.putAll(pathMatchInfo.getUriVariables()); model.putAll(pathMatchInfo.getUriVariables());
} }
model.put("post", postByName(name));
// used by HaloTrackerProcessor // used by HaloTrackerProcessor
model.put("groupVersionKind", groupVersionKind); model.put("groupVersionKind", groupVersionKind);
model.put("plural", gvk.plural()); model.put("plural", gvk.plural());
// used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor // used by TemplateGlobalHeadProcessor and PostTemplateHeadProcessor
model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue()); model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
return postByName(name)
return ServerResponse.ok() .flatMap(postVo -> {
.render(DefaultTemplateEnum.POST.getValue(), model); model.put("post", postVo);
String template = postVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,
DefaultTemplateEnum.POST.getValue())
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
});
}; };
} }
@ -67,7 +74,7 @@ public class PostRouteStrategy implements DetailsPageRouteHandlerStrategy {
} }
private Mono<PostVo> postByName(String name) { private Mono<PostVo> postByName(String name) {
return Mono.defer(() -> Mono.just(postFinder.getByName(name))) return Mono.fromCallable(() -> postFinder.getByName(name))
.publishOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
} }
} }

View File

@ -1,5 +1,6 @@
package run.halo.app.theme.router.strategy; package run.halo.app.theme.router.strategy;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerFunction; import org.springframework.web.reactive.function.server.HandlerFunction;
@ -13,6 +14,7 @@ import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.SinglePageFinder;
import run.halo.app.theme.finders.vo.SinglePageVo; import run.halo.app.theme.finders.vo.SinglePageVo;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template * The {@link SinglePageRouteStrategy} for generate {@link HandlerFunction} specific to the template
@ -25,9 +27,12 @@ import run.halo.app.theme.finders.vo.SinglePageVo;
public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy { public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy {
private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class); private final GroupVersionKind gvk = GroupVersionKind.fromExtension(SinglePage.class);
private final SinglePageFinder singlePageFinder; private final SinglePageFinder singlePageFinder;
private final ViewNameResolver viewNameResolver;
public SinglePageRouteStrategy(SinglePageFinder singlePageFinder) { public SinglePageRouteStrategy(SinglePageFinder singlePageFinder,
ViewNameResolver viewNameResolver) {
this.singlePageFinder = singlePageFinder; this.singlePageFinder = singlePageFinder;
this.viewNameResolver = viewNameResolver;
} }
private String getPlural() { private String getPlural() {
@ -36,22 +41,27 @@ public class SinglePageRouteStrategy implements DetailsPageRouteHandlerStrategy
} }
private Mono<SinglePageVo> singlePageByName(String name) { private Mono<SinglePageVo> singlePageByName(String name) {
return Mono.defer(() -> Mono.just(singlePageFinder.getByName(name))) return Mono.fromCallable(() -> singlePageFinder.getByName(name))
.publishOn(Schedulers.boundedElastic()); .subscribeOn(Schedulers.boundedElastic());
} }
@Override @Override
public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules, public HandlerFunction<ServerResponse> getHandler(SystemSetting.ThemeRouteRules routeRules,
String name) { String name) {
return request -> ServerResponse.ok() return request -> {
.render(DefaultTemplateEnum.SINGLE_PAGE.getValue(), Map<String, Object> model = new HashMap<>();
Map.of("name", name, model.put("groupVersionKind", gvk);
"groupVersionKind", gvk, model.put("plural", getPlural());
"plural", getPlural(), model.put(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue());
"singlePage", singlePageByName(name),
ModelConst.TEMPLATE_ID, DefaultTemplateEnum.SINGLE_PAGE.getValue() return singlePageByName(name).flatMap(singlePageVo -> {
) model.put("singlePage", singlePageVo);
); String template = singlePageVo.getSpec().getTemplate();
return viewNameResolver.resolveViewNameOrDefault(request, template,
DefaultTemplateEnum.SINGLE_PAGE.getValue())
.flatMap(viewName -> ServerResponse.ok().render(viewName, model));
});
};
} }
@Override @Override

View File

@ -2,11 +2,15 @@ package run.halo.app.core.extension;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import org.json.JSONException; import org.json.JSONException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert; import org.skyscreamer.jsonassert.JSONAssert;
import org.springframework.security.util.InMemoryResource;
import run.halo.app.extension.Metadata; import run.halo.app.extension.Metadata;
import run.halo.app.extension.Unstructured;
import run.halo.app.infra.utils.JsonUtils; import run.halo.app.infra.utils.JsonUtils;
import run.halo.app.infra.utils.YamlUnstructuredLoader;
/** /**
* Tests for {@link Theme}. * Tests for {@link Theme}.
@ -74,4 +78,75 @@ class ThemeTest {
assertThat(themeSpec.getVersion()).isEqualTo("1.0.0"); assertThat(themeSpec.getVersion()).isEqualTo("1.0.0");
assertThat(themeSpec.getRequire()).isEqualTo("2.0.0"); assertThat(themeSpec.getRequire()).isEqualTo("2.0.0");
} }
@Test
void themeCustomTemplate() throws JSONException {
String themeYaml = """
apiVersion: theme.halo.run/v1alpha1
kind: Theme
metadata:
name: guqing-higan
spec:
displayName: higan
customTemplates:
post:
- name: post-template-1
description: description for post-template-1
screenshot: foo.png
file: post_template_1.html
- name: post-template-2
description: description for post-template-2
screenshot: bar.png
file: post_template_2.html
category:
- name: category-template-1
description: description for category-template-1
screenshot: foo.png
file: category_template_1.html
page:
- name: page-template-1
description: description for page-template-1
screenshot: foo.png
file: page_template_1.html
""";
List<Unstructured> unstructuredList =
new YamlUnstructuredLoader(new InMemoryResource(themeYaml)).load();
assertThat(unstructuredList).hasSize(1);
Theme theme = Unstructured.OBJECT_MAPPER.convertValue(unstructuredList.get(0), Theme.class);
assertThat(theme).isNotNull();
JSONAssert.assertEquals("""
{
"post": [
{
"name": "post-template-1",
"description": "description for post-template-1",
"screenshot": "foo.png",
"file": "post_template_1.html"
},
{
"name": "post-template-2",
"description": "description for post-template-2",
"screenshot": "bar.png",
"file": "post_template_2.html"
}
],
"category": [
{
"name": "category-template-1",
"description": "description for category-template-1",
"screenshot": "foo.png",
"file": "category_template_1.html"
}],
"page": [
{
"name": "page-template-1",
"description": "description for page-template-1",
"screenshot": "foo.png",
"file": "page_template_1.html"
}]
}
""",
JsonUtils.objectToJson(theme.getSpec().getCustomTemplates()),
true);
}
} }

View File

@ -0,0 +1,93 @@
package run.halo.app.theme.router;
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.when;
import java.net.URI;
import java.net.URISyntaxException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.autoconfigure.thymeleaf.ThymeleafProperties;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.reactive.function.server.MockServerRequest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.reactive.result.view.View;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import run.halo.app.theme.HaloViewResolver;
import run.halo.app.theme.router.strategy.EmptyView;
/**
* Tests for {@link ViewNameResolver}.
*
* @author guqing
* @since 2.0.0
*/
@ExtendWith(SpringExtension.class)
class ViewNameResolverTest {
@Mock
private HaloViewResolver haloViewResolver;
@Mock
private ThymeleafProperties thymeleafProperties;
@InjectMocks
private ViewNameResolver viewNameResolver;
@BeforeEach
void setUp() {
when(thymeleafProperties.getSuffix()).thenReturn(ThymeleafProperties.DEFAULT_SUFFIX);
when(haloViewResolver.resolveViewName(eq("post_news"), any()))
.thenReturn(Mono.just(Mockito.mock(View.class)));
when(haloViewResolver.resolveViewName(eq("post_docs"), any()))
.thenReturn(Mono.just(new EmptyView()));
when(haloViewResolver.resolveViewName(eq("post_nothing"), any()))
.thenReturn(Mono.empty());
}
@Test
void resolveViewNameOrDefault() throws URISyntaxException {
ServerWebExchange exchange = Mockito.mock(ServerWebExchange.class);
MockServerRequest request = MockServerRequest.builder()
.uri(new URI("/")).method(HttpMethod.GET)
.exchange(exchange)
.build();
viewNameResolver.resolveViewNameOrDefault(request, "post_news", "post")
.as(StepVerifier::create)
.expectNext("post_news")
.verifyComplete();
// post_docs.html
String viewName = "post_docs" + thymeleafProperties.getSuffix();
viewNameResolver.resolveViewNameOrDefault(request, viewName, "post")
.as(StepVerifier::create)
.expectNext("post_docs")
.verifyComplete();
viewNameResolver.resolveViewNameOrDefault(request, "post_nothing", "post")
.as(StepVerifier::create)
.expectNext("post")
.verifyComplete();
}
@Test
void processName() {
assertThat(viewNameResolver.processName("post_news")).isEqualTo("post_news");
assertThat(viewNameResolver.processName("post_news" + thymeleafProperties.getSuffix()))
.isEqualTo("post_news");
assertThat(viewNameResolver.processName("post_news.test"))
.isEqualTo("post_news.test");
assertThat(viewNameResolver.processName(null)).isNull();
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import run.halo.app.extension.ListResult; import run.halo.app.extension.ListResult;
import run.halo.app.theme.finders.CategoryFinder;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
/** /**
@ -29,6 +30,9 @@ class CategoryRouteStrategyTest extends RouterStrategyTestSuite {
@Mock @Mock
private PostFinder postFinder; private PostFinder postFinder;
@Mock
private CategoryFinder categoryFinder;
@InjectMocks @InjectMocks
private CategoryRouteStrategy categoryRouteStrategy; private CategoryRouteStrategy categoryRouteStrategy;

View File

@ -12,10 +12,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import run.halo.app.content.TestPost; import run.halo.app.content.TestPost;
import run.halo.app.infra.SystemSetting; import run.halo.app.infra.SystemSetting;
import run.halo.app.theme.DefaultTemplateEnum;
import run.halo.app.theme.finders.PostFinder; import run.halo.app.theme.finders.PostFinder;
import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.finders.vo.PostVo;
import run.halo.app.theme.router.ViewNameResolver;
/** /**
* Tests for {@link PostRouteStrategy}. * Tests for {@link PostRouteStrategy}.
@ -29,11 +32,16 @@ class PostRouteStrategyTest extends RouterStrategyTestSuite {
@Mock @Mock
private PostFinder postFinder; private PostFinder postFinder;
@Mock
private ViewNameResolver viewNameResolver;
@InjectMocks @InjectMocks
private PostRouteStrategy postRouteStrategy; private PostRouteStrategy postRouteStrategy;
@Override @Override
public void setUp() { public void setUp() {
lenient().when(viewNameResolver.resolveViewNameOrDefault(any(), any(), any()))
.thenReturn(Mono.just(DefaultTemplateEnum.POST.getValue()));
lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1())); lenient().when(postFinder.getByName(any())).thenReturn(PostVo.from(TestPost.postV1()));
} }

View File

@ -3,7 +3,6 @@ package run.halo.app.theme.router.strategy;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.when;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
@ -51,7 +50,7 @@ abstract class RouterStrategyTestSuite {
lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP), lenient().when(environmentFetcher.fetch(eq(SystemSetting.ThemeRouteRules.GROUP),
eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules())); eq(SystemSetting.ThemeRouteRules.class))).thenReturn(Mono.just(getThemeRouteRules()));
lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com")); lenient().when(haloProperties.getExternalUrl()).thenReturn(new URI("http://example.com"));
when(viewResolver.resolveViewName(any(), any())) lenient().when(viewResolver.resolveViewName(any(), any()))
.thenReturn(Mono.just(new EmptyView())); .thenReturn(Mono.just(new EmptyView()));
setUp(); setUp();
} }