- 实体/双 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 全绿
208 lines
8.7 KiB
Java
208 lines
8.7 KiB
Java
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();
|
||
}
|
||
}
|