175 lines
6.7 KiB
Java
175 lines
6.7 KiB
Java
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);
|
||
}
|
||
}
|