feat(W1): 皮肤包完整管理-外部主题目录+热注册,zip上传/派生/编辑/删除,ThemePackageValidator收敛,Thymeleaf文件解析器,list增source/inUseCount/editable/usedBySiteNames,T-W1-01..20 全绿299/299
build / build-push (push) Successful in 52s
build / build-push (push) Successful in 52s
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
package com.yunhai.cms.theme;
|
||||
|
||||
import com.yunhai.cms.controller.api.ThemeController;
|
||||
import com.yunhai.cms.repository.CmsSiteRepository;
|
||||
import com.yunhai.cms.service.impl.PortalQueryService;
|
||||
import com.yunhai.cms.service.impl.SiteService;
|
||||
import com.yunhai.cms.service.impl.ThemeService;
|
||||
import com.yunhai.cms.support.CmsIntegrationTestBase;
|
||||
import com.yunhai.cms.support.TestUsers;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
@@ -14,6 +19,7 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
@@ -32,14 +38,33 @@ class ThemeApiIT extends CmsIntegrationTestBase {
|
||||
private ThemeService themeService;
|
||||
@Autowired
|
||||
private ThemeRegistry themeRegistry;
|
||||
@Autowired
|
||||
private ThemePackageService themePackageService;
|
||||
@Autowired
|
||||
private SiteService siteService;
|
||||
@Autowired
|
||||
private PortalQueryService portalQueryService;
|
||||
@Autowired
|
||||
private CmsSiteRepository siteRepository;
|
||||
|
||||
private MockMvc standalone;
|
||||
private java.nio.file.Path themeDir;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
void setUp() throws Exception {
|
||||
purgeTables();
|
||||
// W1:IT 内把外部主题目录指向临时目录(不污染 ${user.dir}/data/themes)
|
||||
themeDir = java.nio.file.Files.createTempDirectory("w1-it-themes");
|
||||
themeRegistry.setThemeDir(themeDir.toString());
|
||||
themeRegistry.reload();
|
||||
themePackageService.setThemeDir(themeDir.toString());
|
||||
standalone = MockMvcBuilders
|
||||
.standaloneSetup(new ThemeController(themeService, themeRegistry)).build();
|
||||
.standaloneSetup(new ThemeController(themeService, themeRegistry, themePackageService)).build();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void cleanThemeDir() {
|
||||
com.yunhai.cms.support.ThemePackageTestSupport.deleteRecursively(themeDir);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
@@ -109,4 +134,139 @@ class ThemeApiIT extends CmsIntegrationTestBase {
|
||||
.andExpect(header().string("Cache-Control", "max-age=3600, public"));
|
||||
}
|
||||
}
|
||||
|
||||
// ===== W1 皮肤包管理(方案 §5.3 T-W1-13..18)=====
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-13: 非超管上传 403")
|
||||
void uploadBySchoolAdminForbidden() throws Exception {
|
||||
org.springframework.mock.web.MockMultipartFile zip =
|
||||
com.yunhai.cms.support.ThemePackageTestSupport.buildZip(
|
||||
"t-api-fb", "t-api-fb", true, true, true, true, true, null);
|
||||
assertThatThrownBy(() -> themePackageService.upload(TestUsers.schoolUser(1), zip))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("仅平台超管");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-14: list 增补字段 source/inUseCount/editable;内置包 editable=false")
|
||||
void listCarriesNewFields() throws Exception {
|
||||
themePackageService.upload(TestUsers.superAdmin(),
|
||||
com.yunhai.cms.support.ThemePackageTestSupport.buildZip(
|
||||
"t-api-ext", "t-api-ext", true, true, true, true, true, null));
|
||||
List<com.yunhai.cms.model.vo.ThemeVo> list = themeService.list();
|
||||
com.yunhai.cms.model.vo.ThemeVo def = list.stream()
|
||||
.filter(v -> "default".equals(v.getName())).findFirst().orElseThrow(IllegalStateException::new);
|
||||
assertThat(def.getSource()).isEqualTo("builtin");
|
||||
assertThat(def.isEditable()).isFalse();
|
||||
assertThat(def.getInUseCount()).isGreaterThanOrEqualTo(0);
|
||||
|
||||
com.yunhai.cms.model.vo.ThemeVo ext = list.stream()
|
||||
.filter(v -> "t-api-ext".equals(v.getName())).findFirst().orElseThrow(IllegalStateException::new);
|
||||
assertThat(ext.getSource()).isEqualTo("external");
|
||||
assertThat(ext.isEditable()).isTrue();
|
||||
assertThat(ext.getInUseCount()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-15: 被用外部包删除拒绝且消息含站点名")
|
||||
void deleteInUsePackageRejected() throws Exception {
|
||||
themePackageService.upload(TestUsers.superAdmin(),
|
||||
com.yunhai.cms.support.ThemePackageTestSupport.buildZip(
|
||||
"t-api-del", "t-api-del", true, true, true, true, true, null));
|
||||
com.yunhai.cms.model.dto.SiteSaveRequest req = new com.yunhai.cms.model.dto.SiteSaveRequest();
|
||||
req.setCode("delsite");
|
||||
req.setParentSiteId(0);
|
||||
req.setSchId(1);
|
||||
req.setSchName("第一中学");
|
||||
req.setName("删除保护门户");
|
||||
req.setTheme("t-api-del");
|
||||
siteService.save(TestUsers.superAdmin(), req);
|
||||
|
||||
assertThatThrownBy(() -> themePackageService.remove(TestUsers.superAdmin(), "t-api-del"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("删除保护门户");
|
||||
assertThat(themeRegistry.contains("t-api-del")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-16: 未被用外部包删除成功——list 不含、目录不存在、引用站点历史快照失效")
|
||||
void deleteUnusedPackageSucceeds() throws Exception {
|
||||
themePackageService.upload(TestUsers.superAdmin(),
|
||||
com.yunhai.cms.support.ThemePackageTestSupport.buildZip(
|
||||
"t-api-ext2", "t-api-ext2", true, true, true, true, true, null));
|
||||
com.yunhai.cms.model.dto.SiteSaveRequest req = new com.yunhai.cms.model.dto.SiteSaveRequest();
|
||||
req.setCode("extsite");
|
||||
req.setParentSiteId(0);
|
||||
req.setSchId(1);
|
||||
req.setSchName("第一中学");
|
||||
req.setName("外部皮肤门户");
|
||||
req.setTheme("t-api-ext2");
|
||||
siteService.save(TestUsers.superAdmin(), req);
|
||||
|
||||
// 删除前快照主题=外部包(进缓存)
|
||||
assertThat(portalQueryService.siteSnapshotByPath("extsite").getTheme()).isEqualTo("t-api-ext2");
|
||||
|
||||
// 站点先换回默认皮肤(保存点发布 SiteContentChangedEvent → 缓存失效)
|
||||
com.yunhai.cms.model.dto.SiteSaveRequest switchBack = new com.yunhai.cms.model.dto.SiteSaveRequest();
|
||||
switchBack.setId(siteRepository.findByCode("extsite").orElseThrow(IllegalStateException::new).getId());
|
||||
switchBack.setCode("extsite");
|
||||
switchBack.setParentSiteId(0);
|
||||
switchBack.setSchId(1);
|
||||
switchBack.setName("外部皮肤门户");
|
||||
switchBack.setTheme(com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME);
|
||||
siteService.save(TestUsers.superAdmin(), switchBack);
|
||||
|
||||
themePackageService.remove(TestUsers.superAdmin(), "t-api-ext2");
|
||||
|
||||
assertThat(themeRegistry.contains("t-api-ext2")).isFalse();
|
||||
assertThat(themeService.list().stream()
|
||||
.noneMatch(v -> "t-api-ext2".equals(v.getName()))).isTrue();
|
||||
assertThat(java.nio.file.Files.exists(themeDir.resolve("t-api-ext2"))).isFalse();
|
||||
// SiteContentChangedEvent 失效链路:缓存清空后重新装配回退 default(历史快照已失效)
|
||||
assertThat(portalQueryService.siteSnapshotByPath("extsite").getTheme())
|
||||
.isEqualTo(com.yunhai.cms.theme.ThemeRegistry.DEFAULT_THEME);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-17: 派生+编辑链路——derive(default→my-skin) → PUT meta → list 与资产反映新值")
|
||||
void deriveAndUpdateMetaChain() throws Exception {
|
||||
themePackageService.derive(TestUsers.superAdmin(), "default", "my-skin");
|
||||
assertThat(themeRegistry.contains("my-skin")).isTrue();
|
||||
assertThat(themeRegistry.sourceOf("my-skin")).isEqualTo("external");
|
||||
|
||||
com.yunhai.cms.model.vo.ThemeVo.ThemeParamVo primary = themeService.list().stream()
|
||||
.filter(v -> "default".equals(v.getName())).findFirst().orElseThrow(IllegalStateException::new)
|
||||
.getParams().stream().filter(p -> "primary".equals(p.getKey()))
|
||||
.findFirst().orElseThrow(IllegalStateException::new);
|
||||
com.yunhai.cms.theme.ThemeParam edited = new com.yunhai.cms.theme.ThemeParam();
|
||||
edited.setKey(primary.getKey());
|
||||
edited.setLabel(primary.getLabel());
|
||||
edited.setType(primary.getType());
|
||||
edited.setCssVar(primary.getCssVar());
|
||||
edited.setDefaultValue("#123456");
|
||||
themePackageService.updateMeta(TestUsers.superAdmin(), "my-skin", "我的皮肤",
|
||||
java.util.Collections.singletonList(edited));
|
||||
|
||||
com.yunhai.cms.model.vo.ThemeVo vo = themeService.list().stream()
|
||||
.filter(v -> "my-skin".equals(v.getName())).findFirst().orElseThrow(IllegalStateException::new);
|
||||
assertThat(vo.getTitle()).isEqualTo("我的皮肤");
|
||||
assertThat(vo.getParams().stream().filter(p -> "primary".equals(p.getKey()))
|
||||
.findFirst().orElseThrow(IllegalStateException::new).getDefaultValue()).isEqualTo("#123456");
|
||||
|
||||
// 外部包资产经代理可取
|
||||
standalone.perform(get("/api/theme/my-skin/asset/screenshot.png"))
|
||||
.andExpect(status().isOk());
|
||||
standalone.perform(get("/api/theme/my-skin/asset/css/main.css"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("T-W1-18: reload 端点权限——校级 403、超管 200")
|
||||
void reloadEndpointPermission() throws Exception {
|
||||
assertThatThrownBy(() -> themePackageService.reload(TestUsers.schoolUser(1)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("仅平台超管");
|
||||
themePackageService.reload(TestUsers.superAdmin());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user