package com.yunhai.cms.portal; import com.yunhai.cms.entity.CmsSite; import com.yunhai.cms.support.CmsIntegrationTestBase; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * 主题微调参数渲染注入(方案 §8.3 C 组 T13-T15,§5.3): * 站点 config_json.theme_params 经快照装配 → head 内联 :root 覆盖串(位于皮肤 CSS 之后)。 */ @DisplayName("ThemeRender - 微调参数 CSS 注入(方案 §5.3)") class ThemeRenderIT extends CmsIntegrationTestBase { @Autowired private MockMvc mockMvc; @BeforeEach void seed() { purgeTables(); } @AfterEach void clean() { purgeTables(); } private void persistSiteWithConfig(String code, String configJson) { CmsSite site = new CmsSite(); site.setCode(code); site.setSchId(1); site.setSchName("学校-1"); site.setName("注入测试门户"); site.setTheme("default"); site.setConfigJson(configJson); site.setStatus(1); siteRepository.saveAndFlush(site); } private String homeHtml(String siteCode) throws Exception { MvcResult result = mockMvc.perform(get("/portal/{code}/", siteCode)) .andExpect(status().isOk()) .andReturn(); return result.getResponse().getContentAsString(StandardCharsets.UTF_8); } @Test @DisplayName("T13: 微调参数注入 CSS 变量且位于皮肤 CSS link 之后") void themeParamsInjectedAfterSkinCss() throws Exception { persistSiteWithConfig("inj1", "{\"theme_params\":{\"primary\":\"#b01a2e\"}}"); String html = homeHtml("inj1"); assertThat(html).as("合法色值应注入").contains("--primary:#b01a2e"); int cssLinkIndex = html.indexOf("css/main.css"); int varIndex = html.indexOf("--primary:#b01a2e"); assertThat(cssLinkIndex).as("皮肤 CSS link 应存在").isGreaterThan(-1); assertThat(varIndex).as("内联覆盖串必须在皮肤 CSS link 之后(层叠后写覆盖)") .isGreaterThan(cssLinkIndex); } @Test @DisplayName("T14: 非法色值不注入(red;} / javascript:alert(1) 防注入)") void illegalColorValuesNotInjected() throws Exception { persistSiteWithConfig("inj2", "{\"theme_params\":{\"primary\":\"red;}}\"}}"); String html = homeHtml("inj2"); assertThat(html).doesNotContain("red;}}").doesNotContain("red;}"); assertThat(html).doesNotContain("--primary:red"); purgeTables(); persistSiteWithConfig("inj3", "{\"theme_params\":{\"primary\":\"javascript:alert(1)\"}}"); String html3 = homeHtml("inj3"); assertThat(html3).doesNotContain("javascript:alert(1)"); assertThat(html3).doesNotContain("--primary:javascript"); } @Test @DisplayName("T15: 未声明 key 不注入(按主题 schema 过滤)") void undeclaredKeysNotInjected() throws Exception { persistSiteWithConfig("inj4", "{\"theme_params\":{\"zzhack\":\"zzvalue\"}}"); String html = homeHtml("inj4"); assertThat(html).doesNotContain("zzhack").doesNotContain("zzvalue"); } }