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/"); } }