Files
YschcmsService/src/test/java/com/yunhai/cms/service/impl/DirtyValidationIT.java
T
2026-09-07 09:09:39 +08:00

175 lines
6.7 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.yunhai.cms.service.impl;
import com.yunhai.cms.entity.CmsArticle;
import com.yunhai.cms.entity.CmsBanner;
import com.yunhai.cms.enums.ArticleStatusEnum;
import com.yunhai.cms.enums.CategoryTypeEnum;
import com.yunhai.cms.model.dto.ArticleSaveRequest;
import com.yunhai.cms.model.dto.BannerSaveRequest;
import com.yunhai.cms.model.dto.CategorySaveRequest;
import com.yunhai.cms.model.dto.DeleteRequest;
import com.yunhai.cms.support.CmsIntegrationTestBase;
import com.yunhai.cms.support.TestUsers;
import com.yunhai.platform.apistd.BizException;
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.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 脏数据健壮性回归(第 2 步 bug 清单 D-3/D-4/D-5/D-6):
* 非法枚举码显式拒绝、定时时间窗校验、删除时轮播引用拦截(统一"被引用即拦"规则)。
*/
@DisplayName("脏数据校验 - 枚举/时间窗/删除引用拦截")
@Transactional
class DirtyValidationIT extends CmsIntegrationTestBase {
@Autowired
private ArticleService articleService;
@Autowired
private CategoryService categoryService;
@Autowired
private BannerService bannerService;
private Integer siteId;
private Integer categoryId;
@BeforeEach
void seed() {
siteId = persistSite("dirty1", 1, "脏数据演示站").getId();
categoryId = persistCategory(siteId, 0, "新闻", CategoryTypeEnum.LIST).getId();
}
private CategorySaveRequest catReq(Integer type) {
CategorySaveRequest r = new CategorySaveRequest();
r.setSiteId(siteId);
r.setParentId(0);
r.setName("新栏目");
r.setType(type);
return r;
}
// ===== D-4 非法枚举码 =====
@Test
@DisplayName("栏目 type=9 显式拒绝(不再静默降级为列表栏目)")
void categorySave_illegalType_rejected() {
CategorySaveRequest r = catReq(9);
assertThatThrownBy(() -> categoryService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("栏目类型非法");
}
@Test
@DisplayName("轮播 linkType=9 显式拒绝(不再误报\"跳转目标必须是数字 ID\")")
void bannerSave_illegalLinkType_rejected() {
BannerSaveRequest r = new BannerSaveRequest();
r.setSiteId(siteId);
r.setPosition("home");
r.setTitle("t");
r.setImageUrl("/upload/x.png");
r.setLinkType(9);
r.setLinkValue("https://example.cn");
assertThatThrownBy(() -> bannerService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("跳转类型非法");
}
// ===== D-5 定时时间窗 =====
@Test
@DisplayName("保存:offlineTime 早于 publishTime 显式拒绝(状态机自洽)")
void articleSave_offlineBeforePublish_rejected() {
ArticleSaveRequest r = new ArticleSaveRequest();
r.setSiteId(siteId);
r.setCategoryId(categoryId);
r.setTitle("时间矛盾");
r.setPublishTime(LocalDateTime.of(2026, 12, 1, 0, 0));
r.setOfflineTime(LocalDateTime.of(2026, 11, 1, 0, 0));
assertThatThrownBy(() -> articleService.save(TestUsers.superAdmin(), r))
.isInstanceOf(BizException.class)
.hasMessageContaining("定时下架时间必须晚于发布时间");
}
@Test
@DisplayName("发布:过期 offlineTime 自愈清空(重发=新生命周期,不再被调度器立即下架)")
void publish_staleOfflineTime_cleared() {
Integer id = articleService.save(TestUsers.superAdmin(), draftReq());
// 直接落库一个矛盾历史数据(模拟脏数据):offlineTime 已过
articleRepository.findById(id).ifPresent(a -> {
a.setOfflineTime(LocalDateTime.now().minusDays(1));
articleRepository.saveAndFlush(a);
});
articleService.publish(TestUsers.superAdmin(), siteId, id);
CmsArticle after = articleRepository.findById(id).orElseThrow(IllegalStateException::new);
assertThat(after.getStatus()).isEqualTo(ArticleStatusEnum.PUBLISHED.getCode());
assertThat(after.getOfflineTime()).isNull();
}
private ArticleSaveRequest draftReq() {
ArticleSaveRequest r = new ArticleSaveRequest();
r.setSiteId(siteId);
r.setCategoryId(categoryId);
r.setTitle("普通草稿");
r.setContent("<p>正文</p>");
return r;
}
// ===== D-6 删除时轮播引用拦截 =====
@Test
@DisplayName("删除文章:被轮播引用时拦截并提示")
void articleDelete_blockedByBannerReference() {
Integer articleId = articleService.save(TestUsers.superAdmin(), draftReq());
persistBannerRef(1, String.valueOf(articleId));
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(articleId);
assertThatThrownBy(() -> articleService.delete(TestUsers.superAdmin(), req))
.isInstanceOf(BizException.class)
.hasMessageContaining("轮播");
}
@Test
@DisplayName("删除栏目:被轮播引用时拦截并提示(与子栏目/文章拦截同一规则)")
void categoryDelete_blockedByBannerReference() {
persistBannerRef(2, String.valueOf(categoryId));
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(categoryId);
assertThatThrownBy(() -> categoryService.delete(TestUsers.superAdmin(), req))
.isInstanceOf(BizException.class)
.hasMessageContaining("轮播");
}
@Test
@DisplayName("删除文章:无轮播引用时正常删除(不误拦)")
void articleDelete_withoutReference_succeeds() {
Integer articleId = articleService.save(TestUsers.superAdmin(), draftReq());
DeleteRequest req = new DeleteRequest();
req.setSiteId(siteId);
req.setId(articleId);
articleService.delete(TestUsers.superAdmin(), req);
assertThat(articleRepository.findById(articleId)).isEmpty();
}
private void persistBannerRef(int linkType, String linkValue) {
CmsBanner b = new CmsBanner();
b.setSiteId(siteId);
b.setPosition("home");
b.setTitle("引用型轮播");
b.setImageUrl("/upload/banner.png");
b.setLinkType(linkType);
b.setLinkValue(linkValue);
b.setSortNo(1);
b.setStatus(1);
bannerRepository.saveAndFlush(b);
}
}