feat: 友链/书库/移动端导航/排行榜等多模块功能
新增模块: - 友链(friendlink)后端处理 + 前端管理页与友链板 - 书库(library):导入、章节、封面、阅读页、横竖版自适应 AdaptiveCoverSlot - 顶栏导航(header_nav)配置与 MobileTabBar/MobileRailDrawers 移动端抽屉 - 排行榜 service 测试、站点页面测试、时间线发布(timeline_release) 其它改动: - 后端 handlers/services 全量小幅调整 - 前端组件、库函数、URL/品牌/站点 URL 工具更新 - Lightbox 图片、MdEntries 条目卡、coverColor 派生色相等前端能力
This commit is contained in:
506
backend/service/library_test.go
Normal file
506
backend/service/library_test.go
Normal file
@@ -0,0 +1,506 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/freefire/jiang13-bbs/model"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func newLibraryTestService(t *testing.T) (*LibraryService, string) {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.LibraryDoc{}, &model.LibraryFile{}, &model.LibrarySection{}); err != nil {
|
||||
t.Fatalf("migrate: %v", err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
s := NewLibraryService(db, dir)
|
||||
if err := s.EnsureDir(); err != nil {
|
||||
t.Fatalf("ensure dir: %v", err)
|
||||
}
|
||||
return s, dir
|
||||
}
|
||||
|
||||
func libPub(b bool) *bool { return &b }
|
||||
|
||||
func libInt(v int) *int { return &v }
|
||||
|
||||
func TestLibraryNormalize(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
mutate func(*LibraryInput)
|
||||
wantErr string
|
||||
}{
|
||||
{"slug 为空", func(i *LibraryInput) { i.Slug = "" }, "slug 不能为空"},
|
||||
{"slug 非法", func(i *LibraryInput) { i.Slug = "Bad_Slug" }, "slug 仅允许"},
|
||||
{"slug 超长", func(i *LibraryInput) { i.Slug = strings.Repeat("a", 65) }, "slug 不能超过"},
|
||||
{"标题为空", func(i *LibraryInput) { i.Title = " " }, "标题不能为空"},
|
||||
{"标题超限", func(i *LibraryInput) { i.Title = strings.Repeat("书", 201) }, "标题不能超过"},
|
||||
{"介绍超限", func(i *LibraryInput) { i.Description = strings.Repeat("介", 20001) }, "介绍不能超过"},
|
||||
{"合法", func(i *LibraryInput) {}, ""},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
in := &LibraryInput{Slug: "how-to-live-better", Title: "怎样生活得更好"}
|
||||
tc.mutate(in)
|
||||
err := in.normalize()
|
||||
if tc.wantErr == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("%s: 不应报错,got %v", tc.name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
t.Fatalf("%s: 期望包含 %q,got %v", tc.name, tc.wantErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibrarySlugUnique(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
if _, err := s.Create(&LibraryInput{Slug: "epub-latest", Title: "A"}); err != nil {
|
||||
t.Fatalf("create a: %v", err)
|
||||
}
|
||||
if _, err := s.Create(&LibraryInput{Slug: "epub-latest", Title: "B"}); err == nil {
|
||||
t.Fatal("重复 slug 应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibrarySlugReuseAfterSoftDelete(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, err := s.Create(&LibraryInput{Slug: "test", Title: "测试"})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if err := s.Delete(doc.ID); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
|
||||
// 软删后重建同 slug:报"已删除"提示而非数据库唯一索引错误
|
||||
_, err = s.Create(&LibraryInput{Slug: "test", Title: "重建"})
|
||||
if err == nil || !strings.Contains(err.Error(), "已删除") {
|
||||
t.Fatalf("软删后重建应提示已删除条目占用,got %v", err)
|
||||
}
|
||||
|
||||
// 彻底删除后可重建
|
||||
if err := s.Purge(doc.ID); err != nil {
|
||||
t.Fatalf("purge: %v", err)
|
||||
}
|
||||
if _, err := s.Create(&LibraryInput{Slug: "test", Title: "重建"}); err != nil {
|
||||
t.Fatalf("purge 后重建同 slug 不应报错: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryPurgeRequiresDeleted(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "alive", Title: "在用"})
|
||||
if err := s.Purge(doc.ID); err == nil || !errors.Is(err, ErrLibraryNotDeleted) {
|
||||
t.Fatalf("未删条目 Purge 应拒绝,got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryPurgeRemovesFilesAndSections(t *testing.T) {
|
||||
s, dir := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "purge-me", Title: "待清"})
|
||||
f, err := s.AddFile(doc.ID, 1, "x.txt", strings.NewReader("x"))
|
||||
if err != nil {
|
||||
t.Fatalf("add file: %v", err)
|
||||
}
|
||||
full := filepath.Join(dir, "library", f.StoredName)
|
||||
if _, err := os.Stat(full); err != nil {
|
||||
t.Fatalf("文件未落盘: %v", err)
|
||||
}
|
||||
if _, err := s.CreateSection(doc.ID, &SectionInput{Title: "第一章"}); err != nil {
|
||||
t.Fatalf("create section: %v", err)
|
||||
}
|
||||
|
||||
if err := s.Delete(doc.ID); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if err := s.Purge(doc.ID); err != nil {
|
||||
t.Fatalf("purge: %v", err)
|
||||
}
|
||||
|
||||
var n int64
|
||||
s.db.Unscoped().Model(&model.LibraryDoc{}).Count(&n)
|
||||
if n != 0 {
|
||||
t.Fatalf("条目行应已清除,got %d", n)
|
||||
}
|
||||
s.db.Unscoped().Model(&model.LibraryFile{}).Count(&n)
|
||||
if n != 0 {
|
||||
t.Fatalf("文件行应已清除,got %d", n)
|
||||
}
|
||||
s.db.Unscoped().Model(&model.LibrarySection{}).Count(&n)
|
||||
if n != 0 {
|
||||
t.Fatalf("章节行应已清除,got %d", n)
|
||||
}
|
||||
if _, err := os.Stat(full); !os.IsNotExist(err) {
|
||||
t.Fatalf("磁盘文件应已删除,err=%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryExtWhitelists(t *testing.T) {
|
||||
if !LibraryExtAllowed("epub") || !LibraryExtAllowed(".md") {
|
||||
t.Fatal("白名单内扩展名应允许")
|
||||
}
|
||||
if LibraryExtAllowed("exe") || LibraryExtAllowed("php") || LibraryExtAllowed("") {
|
||||
t.Fatal("白名单外扩展名应拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryAddFile(t *testing.T) {
|
||||
s, dir := newLibraryTestService(t)
|
||||
doc, err := s.Create(&LibraryInput{Slug: "how-to-live-better", Title: "怎样生活得更好"})
|
||||
if err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
|
||||
// 白名单内:落盘 + 建行
|
||||
f, err := s.AddFile(doc.ID, 1, "如何生活.pdf", strings.NewReader("PDF-DATA"))
|
||||
if err != nil {
|
||||
t.Fatalf("add pdf: %v", err)
|
||||
}
|
||||
if f.Ext != "pdf" || f.Size != 8 {
|
||||
t.Fatalf("文件元数据错误: %+v", f)
|
||||
}
|
||||
if f.MIME != "application/pdf" {
|
||||
t.Fatalf("pdf MIME 应为 application/pdf,got %s", f.MIME)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(dir, "library", f.StoredName)); err != nil {
|
||||
t.Fatalf("文件未落盘: %v", err)
|
||||
}
|
||||
|
||||
// html 存储层 MIME 强制 octet-stream(下载安全)
|
||||
h, err := s.AddFile(doc.ID, 1, "page.html", strings.NewReader("<h1>hi</h1>"))
|
||||
if err != nil {
|
||||
t.Fatalf("add html: %v", err)
|
||||
}
|
||||
if h.MIME != "application/octet-stream" {
|
||||
t.Fatalf("html 下载 MIME 应强制 octet-stream,got %s", h.MIME)
|
||||
}
|
||||
|
||||
// 白名单外拒绝
|
||||
if _, err := s.AddFile(doc.ID, 1, "evil.exe", strings.NewReader("MZ")); err == nil {
|
||||
t.Fatal("exe 应被拒绝")
|
||||
}
|
||||
if _, err := s.AddFile(doc.ID, 1, "noext", strings.NewReader("x")); err == nil {
|
||||
t.Fatal("无扩展名应被拒绝")
|
||||
}
|
||||
|
||||
// 超过每条目上限
|
||||
for i := 0; i < MaxLibraryFilesPerDoc-2; i++ {
|
||||
if _, err := s.AddFile(doc.ID, 1, "f.txt", strings.NewReader("x")); err != nil {
|
||||
t.Fatalf("add #%d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if _, err := s.AddFile(doc.ID, 1, "over.txt", strings.NewReader("x")); err == nil {
|
||||
t.Fatal("超出文件数上限应报错")
|
||||
}
|
||||
|
||||
// 未发布条目的文件公开不可见
|
||||
if _, err := s.GetPublicFile(f.ID); err == nil {
|
||||
t.Fatal("未发布条目的文件不应公开可见")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryPublishedFilterAndOrder(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
if _, err := s.Create(&LibraryInput{Slug: "draft-doc", Title: "草稿"}); err != nil {
|
||||
t.Fatalf("create draft: %v", err)
|
||||
}
|
||||
published, _ := s.Create(&LibraryInput{
|
||||
Slug: "live-doc", Title: "已发布", Published: libPub(true), SortOrder: libInt(1),
|
||||
})
|
||||
|
||||
if _, err := s.AddFile(published.ID, 1, "b.txt", strings.NewReader("b")); err != nil {
|
||||
t.Fatalf("add b: %v", err)
|
||||
}
|
||||
if _, err := s.AddFile(published.ID, 1, "a.txt", strings.NewReader("a")); err != nil {
|
||||
t.Fatalf("add a: %v", err)
|
||||
}
|
||||
|
||||
// 未发布不出现在公开目录
|
||||
list, err := s.ListPublished()
|
||||
if err != nil {
|
||||
t.Fatalf("ListPublished: %v", err)
|
||||
}
|
||||
if len(list) != 1 || list[0].Slug != "live-doc" {
|
||||
t.Fatalf("公开目录应只含 live-doc,got %+v", list)
|
||||
}
|
||||
if list[0].FileCount != 2 {
|
||||
t.Fatalf("列表应带文件计数 2,got %d", list[0].FileCount)
|
||||
}
|
||||
if _, err := s.GetPublishedBySlug("draft-doc"); err == nil {
|
||||
t.Fatal("未发布条目按 slug 不应可见")
|
||||
}
|
||||
|
||||
// 软删后公开彻底不可见
|
||||
if err := s.Delete(published.ID); err != nil {
|
||||
t.Fatalf("delete: %v", err)
|
||||
}
|
||||
if _, err := s.GetPublishedBySlug("live-doc"); err == nil {
|
||||
t.Fatal("软删条目不应可见")
|
||||
}
|
||||
|
||||
// 管理端仍可见(含未发布;软删条目带 Deleted 标记一并列出)
|
||||
all, err := s.ListAll()
|
||||
if err != nil {
|
||||
t.Fatalf("ListAll: %v", err)
|
||||
}
|
||||
if len(all) != 2 {
|
||||
t.Fatalf("管理端应含 draft-doc 与软删的 live-doc,got %+v", all)
|
||||
}
|
||||
bySlug := map[string]LibraryDocDetail{}
|
||||
for _, d := range all {
|
||||
bySlug[d.Slug] = d
|
||||
}
|
||||
if d := bySlug["draft-doc"]; d.Deleted {
|
||||
t.Fatalf("draft-doc 不应标记已删除: %+v", d)
|
||||
}
|
||||
if d := bySlug["live-doc"]; !d.Deleted {
|
||||
t.Fatalf("live-doc 应标记已删除: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryFileOrdering(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "order", Title: "排序", Published: libPub(true)})
|
||||
// b 先传(id 小),a 设 sort_order 靠前
|
||||
b, _ := s.AddFile(doc.ID, 1, "b.txt", strings.NewReader("b"))
|
||||
a, _ := s.AddFile(doc.ID, 1, "a.txt", strings.NewReader("a"))
|
||||
s.db.Model(&model.LibraryFile{}).Where("id = ?", a.ID).Update("sort_order", -1)
|
||||
_ = b
|
||||
|
||||
d, err := s.GetPublishedBySlug("order")
|
||||
if err != nil {
|
||||
t.Fatalf("detail: %v", err)
|
||||
}
|
||||
if len(d.Files) != 2 || d.Files[0].Name != "a.txt" {
|
||||
t.Fatalf("文件应按 sort_order,id 排序: %+v", d.Files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryIncrDownload(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "cnt", Title: "计数", Published: libPub(true)})
|
||||
f, err := s.AddFile(doc.ID, 1, "x.pdf", strings.NewReader("x"))
|
||||
if err != nil {
|
||||
t.Fatalf("add: %v", err)
|
||||
}
|
||||
s.IncrDownload(f.ID)
|
||||
s.IncrDownload(f.ID)
|
||||
got, err := s.GetPublicFile(f.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("get: %v", err)
|
||||
}
|
||||
if got.DownloadCount != 2 {
|
||||
t.Fatalf("下载计数应为 2,got %d", got.DownloadCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryDeleteFile(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "del", Title: "删除"})
|
||||
f, err := s.AddFile(doc.ID, 1, "gone.txt", strings.NewReader("gone"))
|
||||
if err != nil {
|
||||
t.Fatalf("add: %v", err)
|
||||
}
|
||||
if err := s.DeleteFile(f.ID); err != nil {
|
||||
t.Fatalf("delete file: %v", err)
|
||||
}
|
||||
if err := s.DeleteFile(f.ID); err == nil {
|
||||
t.Fatal("重复删除应报不存在")
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 章节(两级树) ----------
|
||||
|
||||
func TestLibrarySectionTwoLevelConstraint(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "book", Title: "书"})
|
||||
|
||||
ch, err := s.CreateSection(doc.ID, &SectionInput{Title: "第一章"})
|
||||
if err != nil {
|
||||
t.Fatalf("create chapter: %v", err)
|
||||
}
|
||||
sec, err := s.CreateSection(doc.ID, &SectionInput{ParentID: &ch.ID, Title: "第一节"})
|
||||
if err != nil {
|
||||
t.Fatalf("create section: %v", err)
|
||||
}
|
||||
if sec.ParentID == nil || *sec.ParentID != ch.ID {
|
||||
t.Fatalf("节的父应为章 %d", ch.ID)
|
||||
}
|
||||
// 节下不能再挂节
|
||||
if _, err := s.CreateSection(doc.ID, &SectionInput{ParentID: &sec.ID, Title: "深层"}); err == nil {
|
||||
t.Fatal("三级嵌套应被拒绝")
|
||||
}
|
||||
// 跨条目父章节
|
||||
doc2, _ := s.Create(&LibraryInput{Slug: "book2", Title: "书2"})
|
||||
if _, err := s.CreateSection(doc2.ID, &SectionInput{ParentID: &ch.ID, Title: "跨条目"}); err == nil {
|
||||
t.Fatal("跨条目父章节应被拒绝")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibrarySectionMoveSwap(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "mv", Title: "排序"})
|
||||
a, _ := s.CreateSection(doc.ID, &SectionInput{Title: "甲"})
|
||||
b, _ := s.CreateSection(doc.ID, &SectionInput{Title: "乙"})
|
||||
c, _ := s.CreateSection(doc.ID, &SectionInput{Title: "丙"})
|
||||
|
||||
if err := s.MoveSection(b.ID, "up"); err != nil {
|
||||
t.Fatalf("move up: %v", err)
|
||||
}
|
||||
secs, _ := s.ListSections(doc.ID)
|
||||
if secs[0].ID != b.ID || secs[1].ID != a.ID || secs[2].ID != c.ID {
|
||||
t.Fatalf("上移后顺序应为 乙甲丙: %+v", titles(secs))
|
||||
}
|
||||
// 边界 no-op
|
||||
if err := s.MoveSection(b.ID, "up"); err != nil {
|
||||
t.Fatalf("边界上移: %v", err)
|
||||
}
|
||||
if err := s.MoveSection(b.ID, "bad"); err == nil {
|
||||
t.Fatal("非法方向应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func titles(secs []model.LibrarySection) []string {
|
||||
out := make([]string, len(secs))
|
||||
for i, sec := range secs {
|
||||
out[i] = sec.Title
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestLibraryImportMarkdown(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "imp", Title: "导入"})
|
||||
|
||||
// h2 拆分:围栏内 ## 不拆;首标题前内容并入第一章
|
||||
md := "## 前言\n\n引言正文。\n\n```\n## 不是标题\n```\n\n## 第二章\n\n内容二。"
|
||||
secs, err := s.ImportMarkdown(doc.ID, "how.md", md, "h2")
|
||||
if err != nil {
|
||||
t.Fatalf("import: %v", err)
|
||||
}
|
||||
if len(secs) != 2 {
|
||||
t.Fatalf("应拆出 2 章,got %d: %+v", len(secs), titles(secs))
|
||||
}
|
||||
if secs[0].Title != "前言" || !strings.Contains(secs[0].Content, "引言正文") {
|
||||
t.Fatalf("第一章错误: %+v", secs[0])
|
||||
}
|
||||
if !strings.Contains(secs[0].Content, "不是标题") {
|
||||
t.Fatal("围栏内的 ## 行应留在第一章正文,不应被拆分")
|
||||
}
|
||||
if secs[1].Title != "第二章" {
|
||||
t.Fatalf("第二章标题错误: %+v", secs[1])
|
||||
}
|
||||
|
||||
// 无命中标题 → 回退单章,标题取文件名
|
||||
secs2, err := s.ImportMarkdown(doc.ID, "README.md", "只有正文,没有标题。", "h1")
|
||||
if err != nil {
|
||||
t.Fatalf("import fallback: %v", err)
|
||||
}
|
||||
if len(secs2) != 1 || secs2[0].Title != "README" {
|
||||
t.Fatalf("应回退单章 README: %+v", titles(secs2))
|
||||
}
|
||||
|
||||
// 非法 splitLevel
|
||||
if _, err := s.ImportMarkdown(doc.ID, "x.md", "x", "h3"); err == nil {
|
||||
t.Fatal("非法拆分方式应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryImportMarkdownBatch(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "impb", Title: "批量导入"})
|
||||
|
||||
files := []ImportFileInput{
|
||||
{Filename: "01-不想早死.md", Content: "## 甲\n\n内容甲。"},
|
||||
{Filename: "02-不要慢性自杀.md", Content: "只有正文,没有标题。"},
|
||||
{Filename: "03.md", Content: "# 丙\n\n内容丙。"},
|
||||
}
|
||||
secs, err := s.ImportMarkdownBatch(doc.ID, files, "h2")
|
||||
if err != nil {
|
||||
t.Fatalf("batch import: %v", err)
|
||||
}
|
||||
// 文件1 按 h2 拆出 1 章;文件2、文件3 未命中 ## → 回退单章(标题取文件名)
|
||||
if len(secs) != 3 {
|
||||
t.Fatalf("应创建 3 章,got %d: %+v", len(secs), titles(secs))
|
||||
}
|
||||
if secs[0].Title != "甲" {
|
||||
t.Fatalf("文件1 应按 h2 拆出章: %+v", titles(secs))
|
||||
}
|
||||
if secs[1].Title != "02-不要慢性自杀" {
|
||||
t.Fatalf("文件2 标题应取文件名: %+v", titles(secs))
|
||||
}
|
||||
if secs[2].Title != "03" {
|
||||
t.Fatalf("文件3 未命中 ## 应回退单章: %+v", titles(secs))
|
||||
}
|
||||
for i, sec := range secs {
|
||||
if sec.SortOrder != i {
|
||||
t.Fatalf("sort_order 应按文件顺序连续: %+v", titles(secs))
|
||||
}
|
||||
}
|
||||
|
||||
// 文件数超限
|
||||
many := make([]ImportFileInput, MaxImportFiles+1)
|
||||
for i := range many {
|
||||
many[i] = ImportFileInput{Filename: "f.md", Content: "x"}
|
||||
}
|
||||
if _, err := s.ImportMarkdownBatch(doc.ID, many, "none"); err == nil {
|
||||
t.Fatal("文件数超过上限应报错")
|
||||
}
|
||||
|
||||
// 空文件列表
|
||||
if _, err := s.ImportMarkdownBatch(doc.ID, nil, "none"); err == nil {
|
||||
t.Fatal("空文件列表应报错")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryDeleteSectionCascade(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "cas", Title: "级联"})
|
||||
ch, _ := s.CreateSection(doc.ID, &SectionInput{Title: "章"})
|
||||
sec, _ := s.CreateSection(doc.ID, &SectionInput{ParentID: &ch.ID, Title: "节"})
|
||||
|
||||
if err := s.DeleteSection(ch.ID); err != nil {
|
||||
t.Fatalf("delete chapter: %v", err)
|
||||
}
|
||||
// 章与节都被软删
|
||||
if _, err := s.section(ch.ID); err == nil {
|
||||
t.Fatal("章应已删除")
|
||||
}
|
||||
if _, err := s.section(sec.ID); err == nil {
|
||||
t.Fatal("章删除时其下小节应级联删除")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLibraryDetailCarriesSections(t *testing.T) {
|
||||
s, _ := newLibraryTestService(t)
|
||||
doc, _ := s.Create(&LibraryInput{Slug: "carry", Title: "携带", Published: libPub(true)})
|
||||
if _, err := s.CreateSection(doc.ID, &SectionInput{Title: "章一", Content: "正文一"}); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
d, err := s.GetPublishedBySlug("carry")
|
||||
if err != nil {
|
||||
t.Fatalf("detail: %v", err)
|
||||
}
|
||||
if len(d.Sections) != 1 || d.Sections[0].Content != "正文一" {
|
||||
t.Fatalf("公开详情应携带章节: %+v", d.Sections)
|
||||
}
|
||||
// 草稿条目不可见
|
||||
doc2, _ := s.Create(&LibraryInput{Slug: "draft", Title: "草稿书"})
|
||||
if _, err := s.CreateSection(doc2.ID, &SectionInput{Title: "草稿章"}); err != nil {
|
||||
t.Fatalf("create: %v", err)
|
||||
}
|
||||
if _, err := s.GetPublishedBySlug("draft"); err == nil {
|
||||
t.Fatal("草稿条目不应出现在公开详情")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user