Files
YschcmsService/src/test/java/com/yunhai/cms/service/impl/LinkServiceIT.java
T
chenxing b4822a7f38 feat(B2): 附件引用模型-cms_attachment 增 ref_type/ref_id,消灭孤儿误报
- 实体/双 SQL 加列+idx_att_ref;upload 增 scene 白名单(banner/link/site)
- AttachmentService 新增 markUsed/release/markSiteConfigUsed(webUrl 反查,幂等)
- 未使用=articleId 与 refType 均空;page 返回 unusedCount(前后端同步改名无兼容别名)
- Banner/Link 保存先解引用再回写(换图旧图回未使用);Site 配置 logo/favicon 引用回写(@Lazy 解循环依赖)
- 新增13例(Attachment6/Banner3/Link2/Site2)+既有语义断言更新;mvn -o test 全绿
2026-09-07 19:40:14 +08:00

208 lines
8.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.model.dto.DeleteRequest;
import com.yunhai.cms.model.dto.LinkSaveRequest;
import com.yunhai.cms.model.vo.LinkVo;
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.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* 友情链接管理集成测试(方案 §5.1 表6 / §9.2)。
*/
@DisplayName("LinkService - 友情链接管理")
@Transactional
class LinkServiceIT extends CmsIntegrationTestBase {
@Autowired
private LinkService linkService;
private Integer siteId;
@BeforeEach
void seed() {
siteId = persistSite("demo1", 1, "演示站点一").getId();
}
private LinkSaveRequest req(String name, String url) {
LinkSaveRequest r = new LinkSaveRequest();
r.setSiteId(siteId);
r.setName(name);
r.setLinkUrl(url);
return r;
}
@Test
@DisplayName("保存友链:http/https/站内相对路径均接受;分组与默认值生效")
void save_urlSchemesAndDefaults() {
assertThat(linkService.save(TestUsers.superAdmin(), req("教育部", "http://www.moe.gov.cn")).getId())
.isNotNull();
assertThat(linkService.save(TestUsers.superAdmin(), req("兄弟院校", "https://www.pku.edu.cn")).getId())
.isNotNull();
LinkVo relative = linkService.save(TestUsers.superAdmin(), req("站内页", "/cms/portal/demo1/"));
assertThat(relative.getGroupName()).isEqualTo("default"); // 分组缺省
assertThat(relative.getSortNo()).isZero();
assertThat(relative.getStatus()).isEqualTo(1);
}
@Test
@DisplayName("保存友链:javascript: 等非法协议与空地址被拒")
void save_invalidUrlRejected() {
assertThatThrownBy(() -> linkService.save(
TestUsers.superAdmin(), req("危险链", "javascript:alert(1)")))
.isInstanceOf(BizException.class)
.hasMessageContaining("http");
assertThatThrownBy(() -> linkService.save(TestUsers.superAdmin(), req("空链", "")))
.isInstanceOf(BizException.class)
.hasMessageContaining("http");
assertThatThrownBy(() -> linkService.save(
TestUsers.superAdmin(), req("数据协议", "data:text/html,<script>")))
.isInstanceOf(BizException.class)
.hasMessageContaining("http");
}
@Test
@DisplayName("列表按分组名+组内排序返回全量(含隐藏,供管理端)")
void list_orderedByGroupAndSort() {
LinkSaveRequest b = req("B链", "https://b.example.com");
b.setGroupName("上级部门");
b.setSortNo(2);
linkService.save(TestUsers.superAdmin(), b);
LinkSaveRequest a = req("A链", "https://a.example.com");
a.setGroupName("上级部门");
a.setSortNo(1);
linkService.save(TestUsers.superAdmin(), a);
LinkSaveRequest coop = req("合作单位链", "https://c.example.com");
coop.setGroupName("合作单位");
linkService.save(TestUsers.superAdmin(), coop);
LinkSaveRequest hidden = req("隐藏链", "https://h.example.com");
hidden.setStatus(0);
linkService.save(TestUsers.superAdmin(), hidden);
List<LinkVo> list = linkService.list(TestUsers.superAdmin(), siteId);
// 分组名升序走 MySQL utf8mb4_general_ci:ASCII "default" < 中文(上 U+4E0A < 合 U+5408)
assertThat(list).extracting(LinkVo::getName)
.containsExactly("隐藏链", "A链", "B链", "合作单位链");
}
@Test
@DisplayName("修改/删除他站友链抛 FORBIDDEN;删除本站成功")
void crossSiteAndDelete() {
LinkVo own = linkService.save(TestUsers.superAdmin(), req("待删链", "https://x.example.com"));
Integer otherSiteId = persistSite("demo2", 2, "演示站点二").getId();
LinkSaveRequest hijack = req("越权改链", "https://y.example.com");
hijack.setId(own.getId());
hijack.setSiteId(otherSiteId);
assertThatThrownBy(() -> linkService.save(TestUsers.superAdmin(), hijack))
.isInstanceOf(BizException.class)
.hasMessageContaining("无权");
DeleteRequest wrongSite = new DeleteRequest();
wrongSite.setSiteId(otherSiteId);
wrongSite.setId(own.getId());
assertThatThrownBy(() -> linkService.delete(TestUsers.superAdmin(), wrongSite))
.isInstanceOf(BizException.class)
.hasMessageContaining("无权");
DeleteRequest del = new DeleteRequest();
del.setSiteId(siteId);
del.setId(own.getId());
linkService.delete(TestUsers.superAdmin(), del);
assertThat(linkRepository.findById(own.getId())).isEmpty();
}
@Test
@DisplayName("删除不存在的友链报错(边界)")
void delete_missingRejected() {
DeleteRequest del = new DeleteRequest();
del.setSiteId(siteId);
del.setId(99999);
assertThatThrownBy(() -> linkService.delete(TestUsers.superAdmin(), del))
.isInstanceOf(BizException.class)
.hasMessageContaining("不存在");
}
// ===== B2 附件引用模型(友链 logo 引用回写)=====
@Autowired
private AttachmentService attachmentService;
/** 1x1 真实 png(上传需过魔数校验) */
private org.springframework.mock.web.MockMultipartFile pngFile() {
java.awt.image.BufferedImage img = new java.awt.image.BufferedImage(1, 1, java.awt.image.BufferedImage.TYPE_INT_RGB);
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream();
try {
javax.imageio.ImageIO.write(img, "png", out);
} catch (java.io.IOException e) {
throw new IllegalStateException(e);
}
return new org.springframework.mock.web.MockMultipartFile("file", "logo.png", "image/png", out.toByteArray());
}
@Test
@DisplayName("B2: 保存友链回写 attachment.ref_type=link, ref_id=linkId;删除解引用")
void save_writesAttachmentRef_deleteReleases() {
com.yunhai.cms.model.vo.AttachmentVo logo = attachmentService.upload(
TestUsers.superAdmin(), siteId, null, "link", pngFile());
LinkSaveRequest r = req("带Logo友链", "https://www.example.com");
r.setLogoUrl(logo.getUrl());
LinkVo vo = linkService.save(TestUsers.superAdmin(), r);
com.yunhai.cms.entity.CmsAttachment att = attachmentRepository.findById(logo.getId())
.orElseThrow(IllegalStateException::new);
assertThat(att.getRefType()).isEqualTo("link");
assertThat(att.getRefId()).isEqualTo(vo.getId());
DeleteRequest del = new DeleteRequest();
del.setSiteId(siteId);
del.setId(vo.getId());
linkService.delete(TestUsers.superAdmin(), del);
com.yunhai.cms.entity.CmsAttachment released = attachmentRepository.findById(logo.getId())
.orElseThrow(IllegalStateException::new);
assertThat(released.getRefType()).isNull();
assertThat(released.getRefId()).isNull();
}
@Test
@DisplayName("B2: 友链无 logo 不产生附件引用;logo 清空后引用解除")
void save_withoutLogo_noRef() {
LinkSaveRequest r = req("纯文字友链", "https://www.example.com");
LinkVo vo = linkService.save(TestUsers.superAdmin(), r);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId())).isEmpty();
com.yunhai.cms.model.vo.AttachmentVo logo = attachmentService.upload(
TestUsers.superAdmin(), siteId, null, "link", pngFile());
LinkSaveRequest withLogo = req("先有Logo", "https://www.example.com");
withLogo.setId(vo.getId());
withLogo.setLogoUrl(logo.getUrl());
linkService.save(TestUsers.superAdmin(), withLogo);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId()))
.extracting(com.yunhai.cms.entity.CmsAttachment::getId)
.containsExactly(logo.getId());
// logo 清空(改回纯文字)→ 引用解除
LinkSaveRequest clear = req("再去Logo", "https://www.example.com");
clear.setId(vo.getId());
clear.setLogoUrl("");
linkService.save(TestUsers.superAdmin(), clear);
assertThat(attachmentRepository.findByRefTypeAndRefId("link", vo.getId())).isEmpty();
}
}