package com.yunhai.cms.utils;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* 门户 URL 生成器单测(方案 §8.6 前置约定2:所有链接经 PortalUrlHelper 生成)。
* 覆盖:请求派生形态、public_base_url 学校域形态、各 URL 段拼接、编码与边界;
* 资产 URL 主题段(B-3)、上传地址补基准与正文重写(B-1)。
*/
@DisplayName("PortalUrlHelper - 门户 URL 生成")
class PortalUrlHelperTest {
private static MockHttpServletRequest request(String scheme, String server, int port,
String contextPath) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setScheme(scheme);
request.setServerName(server);
request.setServerPort(port);
request.setContextPath(contextPath);
return request;
}
// ===== 请求派生形态(public_base_url 未配置,预览/平台内形态)=====
@Test
@DisplayName("从请求派生基地址:非默认端口 + contextPath 均保留")
void fromRequest_keepsNonDefaultPortAndContext() {
PortalUrlHelper url = PortalUrlHelper.fromRequest(
request("http", "localhost", 8072, "/cms"), "demo", "default");
assertThat(url.getBaseUrl()).isEqualTo("http://localhost:8072/cms/portal/demo");
assertThat(url.home()).isEqualTo("http://localhost:8072/cms/portal/demo/");
assertThat(url.category(5)).isEqualTo("http://localhost:8072/cms/portal/demo/c/5");
assertThat(url.article(9)).isEqualTo("http://localhost:8072/cms/portal/demo/a/9");
}
@Test
@DisplayName("http:80 / https:443 默认端口省略")
void fromRequest_omitsDefaultPorts() {
assertThat(PortalUrlHelper.fromRequest(request("http", "a.com", 80, ""), "s1", "default").home())
.isEqualTo("http://a.com/portal/s1/");
assertThat(PortalUrlHelper.fromRequest(request("https", "a.com", 443, ""), "s1", "default").home())
.isEqualTo("https://a.com/portal/s1/");
}
@Test
@DisplayName("search:关键词 URL 编码(UTF-8),page=1 不带页码、page>1 追加")
void search_encodesKeywordAndAppendsPageOnlyWhenBeyondFirst() throws Exception {
PortalUrlHelper url = new PortalUrlHelper("https://base.example.com/cms/portal/demo", "demo", "default");
assertThat(url.search("开学 典礼", 1))
.isEqualTo("https://base.example.com/cms/portal/demo/search?q="
+ java.net.URLEncoder.encode("开学 典礼", "UTF-8"));
assertThat(url.search("x", 3))
.isEqualTo("https://base.example.com/cms/portal/demo/search?q=x&page=3");
assertThat(url.search(null, null))
.isEqualTo("https://base.example.com/cms/portal/demo/search?q=");
}
// ===== 学校独立域名形态(config_json.public_base_url)=====
@Test
@DisplayName("public_base_url 形态:站点路径段被剥离,直接以学校域为基准")
void publicBaseUrl_formUsesSchoolDomainAsBase() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn/", "demo", "classic-red");
assertThat(url.home()).isEqualTo("https://www.xxx.edu.cn/");
assertThat(url.category(3)).isEqualTo("https://www.xxx.edu.cn/c/3");
assertThat(url.article(7)).isEqualTo("https://www.xxx.edu.cn/a/7");
assertThat(url.getSiteCode()).isEqualTo("demo");
}
@Test
@DisplayName("asset 入参前导斜杠兼容,重复尾斜杠的 base 归一")
void assetAndBaseTrimming() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn///", "demo", "default");
assertThat(url.getBaseUrl()).isEqualTo("https://www.xxx.edu.cn");
assertThat(url.asset("/css/a.css")).isEqualTo("https://www.xxx.edu.cn/assets/default/css/a.css");
assertThat(url.asset("css/a.css")).isEqualTo("https://www.xxx.edu.cn/assets/default/css/a.css");
}
@Test
@DisplayName("blank base 容错:不因空白 base 抛异常,路径仍以 / 起头")
void blankBaseTolerated() {
PortalUrlHelper url = new PortalUrlHelper(null, "demo", "default");
assertThat(url.home()).isEqualTo("/");
assertThat(url.article(1)).isEqualTo("/a/1");
}
// ===== 资产 URL 主题段(B-3:换肤即换 URL,浏览器/CDN 缓存天然失效)=====
@Test
@DisplayName("asset 含主题段:theme=default 与 classic-red 生成不同 URL")
void asset_urlCarriesThemeSegment() {
PortalUrlHelper url = new PortalUrlHelper("http://localhost:8072/cms/portal/demo", "demo", "default");
assertThat(url.asset("css/main.css"))
.isEqualTo("http://localhost:8072/cms/portal/demo/assets/default/css/main.css");
PortalUrlHelper switched = new PortalUrlHelper("http://localhost:8072/cms/portal/demo", "demo", "classic-red");
assertThat(switched.asset("css/main.css"))
.isEqualTo("http://localhost:8072/cms/portal/demo/assets/classic-red/css/main.css");
}
@Test
@DisplayName("asset 主题段空值回退 default(防御:theme 字段异常不致 404)")
void asset_blankThemeFallsBackToDefault() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", null);
assertThat(url.asset("css/main.css")).isEqualTo("https://www.xxx.edu.cn/assets/default/css/main.css");
}
// ===== 上传地址补基准(B-1:webUrl 只存相对路径,渲染期补全)=====
@Test
@DisplayName("upload:相对路径按基准 origin 补全(base 域形态 / 学校域形态)")
void upload_relativePathPrefixedWithOrigin() {
PortalUrlHelper base = new PortalUrlHelper("http://localhost:8072/cms/portal/demo", "demo", "default");
assertThat(base.upload("/cms/upload/cms/demo/202609/abc.jpg"))
.isEqualTo("http://localhost:8072/cms/upload/cms/demo/202609/abc.jpg");
PortalUrlHelper school = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", "default");
assertThat(school.upload("/cms/upload/cms/demo/202609/abc.jpg"))
.isEqualTo("https://www.xxx.edu.cn/cms/upload/cms/demo/202609/abc.jpg");
}
@Test
@DisplayName("upload:已是完整 http(s) 地址原样返回(外链图不误改);空值原样返回")
void upload_absoluteAndBlankUntouched() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", "default");
assertThat(url.upload("https://cdn.example.com/img.png")).isEqualTo("https://cdn.example.com/img.png");
assertThat(url.upload("http://otherhost:8080/cms/upload/a.jpg")).isEqualTo("http://otherhost:8080/cms/upload/a.jpg");
assertThat(url.upload(null)).isNull();
assertThat(url.upload("")).isEqualTo("");
assertThat(url.upload(" ")).isEqualTo(" ");
}
@Test
@DisplayName("rewriteUploadUrls:正文内相对/历史绝对上传地址统一重写为当前基准,外链不动")
void rewriteUploadUrls_normalizesLegacyAndRelative() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", "default");
String html = "

"
+ "
"
+ "
";
String rewritten = url.rewriteUploadUrls(html);
assertThat(rewritten).contains("src=\"https://www.xxx.edu.cn/cms/upload/cms/demo/202609/a.jpg\"");
assertThat(rewritten).contains("src=\"https://www.xxx.edu.cn/cms/upload/cms/demo/202609/b.jpg\"");
assertThat(rewritten).doesNotContain("localhost:8072");
assertThat(rewritten).contains("src=\"https://cdn.example.com/x.png\"");
assertThat(url.rewriteUploadUrls(null)).isNull();
}
// ===== forPreview 预览资产前缀(W3:context-path 修复,方案 §7.3)=====
@Test
@DisplayName("T-W3-01: forPreview 带上下文:资产前缀含 contextPath")
void forPreview_assetPrefixCarriesContextPath() {
PortalUrlHelper url = PortalUrlHelper.forPreview(
request("http", "base.yunhaixingtong.com", 80, "/yschcms"), "demo", "scholar-blue");
assertThat(url.asset("css/main.css"))
.isEqualTo("/yschcms/api/theme/scholar-blue/asset/css/main.css");
}
@Test
@DisplayName("T-W3-02: forPreview 空上下文退化:前缀 /api/theme/…(dev 形态不回归)")
void forPreview_emptyContextFallsBackToRootPrefix() {
PortalUrlHelper url = PortalUrlHelper.forPreview(
request("http", "localhost", 8072, ""), "demo", "t-demo-red");
assertThat(url.asset("css/main.css"))
.isEqualTo("/api/theme/t-demo-red/asset/css/main.css");
}
@Test
@DisplayName("T-W3-03: forPreview 站内链接不受资产前缀串味(home/category 仍指真实门户)")
void forPreview_homeStaysRealPortal() {
PortalUrlHelper url = PortalUrlHelper.forPreview(
request("http", "base.yunhaixingtong.com", 80, "/yschcms"), "demo", "scholar-blue");
assertThat(url.home()).isEqualTo("http://base.yunhaixingtong.com/yschcms/portal/demo/");
assertThat(url.category(3)).isEqualTo("http://base.yunhaixingtong.com/yschcms/portal/demo/c/3");
assertThat(url.search("x", null))
.isEqualTo("http://base.yunhaixingtong.com/yschcms/portal/demo/search?q=x");
}
// ===== siteHome 分支(B-4:学校域形态下未配域子站不再退化为 "/")=====
@Test
@DisplayName("siteHome 四分支:目标配域 / base域替换站点段 / 学校域退化 base 域规范路径 / 无配置回 /")
void siteHome_branches() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "main", "default",
"https://base.yunhaixingtong.com/cms");
// ① 目标站配置 public_base_url:以它为准(空白视同未配)
assertThat(url.siteHome("jdx", "https://jdx.xxx.edu.cn")).isEqualTo("https://jdx.xxx.edu.cn/");
assertThat(url.siteHome("jdx", " ")).isEqualTo("https://base.yunhaixingtong.com/cms/portal/jdx/");
// ② 学校域形态(当前基准无 /portal/ 段)+ 目标未配域 → base 域规范路径(B-4 修复)
assertThat(url.siteHome("jdx", null)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/jdx/");
// ③ base 域子路径形态:替换站点段(既有行为不变)
PortalUrlHelper sub = new PortalUrlHelper("https://base.yunhaixingtong.com/cms/portal/main", "main",
"default", "https://base.yunhaixingtong.com/cms");
assertThat(sub.siteHome("jdx", null)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/jdx/");
// ④ 学校域形态 + base-origin 未配置 → 退回 "/"(现状兜底)
PortalUrlHelper noBase = new PortalUrlHelper("https://www.xxx.edu.cn", "main", "default", null);
assertThat(noBase.siteHome("jdx", null)).isEqualTo("/");
}
@Test
@DisplayName("fromBaseOrigin:C-6 未配 public_base_url 时派生 base 域规范路径")
void fromBaseOrigin_derivesCanonicalBaseDomainPath() {
PortalUrlHelper url = PortalUrlHelper.fromBaseOrigin(
"https://base.yunhaixingtong.com/cms/", "demo", "default");
assertThat(url.home()).isEqualTo("https://base.yunhaixingtong.com/cms/portal/demo/");
assertThat(url.category(3)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/demo/c/3");
assertThat(url.asset("css/main.css"))
.isEqualTo("https://base.yunhaixingtong.com/cms/portal/demo/assets/default/css/main.css");
}
// ===== 服务中心(事项库 M0,详细设计 §5.4:模板不拼 query,URL 由后端预生成)=====
@Test
@DisplayName("serviceList/serviceItem:基址与指南页路径拼接")
void serviceList_andServiceItem_paths() {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", "default");
assertThat(url.serviceList()).isEqualTo("https://www.xxx.edu.cn/service");
assertThat(url.serviceItem(9)).isEqualTo("https://www.xxx.edu.cn/sv/9");
// 嵌套 sitePath 形态同既有路由
PortalUrlHelper nested = new PortalUrlHelper(
"https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy", "sch-3/jdxy", "default");
assertThat(nested.serviceList()).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/service");
assertThat(nested.serviceItem(12)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/sv/12");
}
@Test
@DisplayName("serviceList(Map):null/空串值跳过、中文 URL 编码、& 连接")
void serviceList_mapSkipsBlankAndEncodes() throws Exception {
PortalUrlHelper url = new PortalUrlHelper("https://www.xxx.edu.cn", "demo", "default");
java.util.Map query = new java.util.LinkedHashMap<>();
query.put("obj", "个人");
query.put("cat", null);
query.put("dept", "");
query.put("q", "转 学");
query.put("page", 2);
assertThat(url.serviceList(query)).isEqualTo(
"https://www.xxx.edu.cn/service?obj=" + java.net.URLEncoder.encode("个人", "UTF-8")
+ "&q=" + java.net.URLEncoder.encode("转 学", "UTF-8") + "&page=2");
// 全空值 = 纯基址(不带尾问号)
java.util.Map blanks = new java.util.LinkedHashMap<>();
blanks.put("obj", null);
blanks.put("q", "");
assertThat(url.serviceList(blanks)).isEqualTo("https://www.xxx.edu.cn/service");
assertThat(url.serviceList(null)).isEqualTo("https://www.xxx.edu.cn/service");
assertThat(url.serviceList(new java.util.HashMap<>())).isEqualTo("https://www.xxx.edu.cn/service");
}
// ===== W4 嵌套路径(方案 §8.3 T-W4-11/12:构造参语义 code→sitePath)=====
@Test
@DisplayName("T-W4-11: 嵌套 sitePath 的 baseUrl/asset/category 全嵌套")
void nestedSitePath_allUrlsNested() {
PortalUrlHelper url = PortalUrlHelper.fromBaseOrigin(
"https://base.yunhaixingtong.com/cms", "sch-3/jdxy", "scholar-blue");
assertThat(url.home()).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/");
assertThat(url.category(7)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/c/7");
assertThat(url.article(9)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/a/9");
assertThat(url.asset("css/main.css"))
.isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy/assets/scholar-blue/css/main.css");
}
@Test
@DisplayName("T-W4-12: siteHome 兄弟/父级——当前=sch-3/jdxy 时替换站点段为嵌套目标")
void nestedSitePath_siteHomeSiblingsAndParent() {
PortalUrlHelper url = new PortalUrlHelper(
"https://base.yunhaixingtong.com/cms/portal/sch-3/jdxy", "sch-3/jdxy", "default",
"https://base.yunhaixingtong.com/cms");
// 父级主站
assertThat(url.siteHome("sch-3", null)).isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/");
// 兄弟子站(嵌套形态同构)
assertThat(url.siteHome("sch-3/other", null))
.isEqualTo("https://base.yunhaixingtong.com/cms/portal/sch-3/other/");
// 目标配 public_base_url 时优先
assertThat(url.siteHome("sch-3/other", "https://other.xxx.edu.cn")).isEqualTo("https://other.xxx.edu.cn/");
}
}