Files
jiang13-bbs/backend/service/library_export_test.go
freefire ff2ab286fb feat: 书库导入导出/图片变体/书籍搜索/小组件运行时等
新增:
- 书库导入导出(library_import/library_export)及测试
- 图片变体生成(image_variants)与响应式图片(responsiveImage)
- 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件
- 小组件运行时(widgetRuntime)与静态检查(widgetLint)
- 上传缓存中间件(upload_cache)与字体 CSS 提取脚本

其它:
- 后端 handlers/services 全量调整
- 前端页面、组件、库函数与配置更新
2026-10-01 03:06:05 +08:00

200 lines
5.9 KiB
Go
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 service
import (
"archive/zip"
"bytes"
"encoding/json"
"errors"
"io"
"os"
"path/filepath"
"testing"
)
// readZipEntry 读取 ZIP 内指定名称的文件内容(不存在返回 nil, false)
func readZipEntry(t *testing.T, zr *zip.Reader, name string) ([]byte, bool) {
t.Helper()
for _, f := range zr.File {
if f.Name == name {
rc, err := f.Open()
if err != nil {
t.Fatalf("open zip entry %s: %v", name, err)
}
defer rc.Close()
data, err := io.ReadAll(rc)
if err != nil {
t.Fatalf("read zip entry %s: %v", name, err)
}
return data, true
}
}
return nil, false
}
func buildExportZip(t *testing.T, exp *LibraryExport) *zip.Reader {
t.Helper()
var buf bytes.Buffer
if err := exp.WriteZip(&buf); err != nil {
t.Fatalf("write zip: %v", err)
}
zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
if err != nil {
t.Fatalf("read exported zip: %v", err)
}
return zr
}
// seedBookForExport 造一本含章节(章 + 小节)、附件与本地封面的书籍
func seedBookForExport(t *testing.T, s *LibraryService, uploads string) (docID uint, storedName string) {
t.Helper()
pub := true
doc, err := s.Create(&LibraryInput{Slug: "dao-jia", Title: "道家研究", Author: "老子", Description: "# 介绍"})
if err != nil {
t.Fatalf("create doc: %v", err)
}
coverDir := filepath.Join(uploads, "images")
if err := os.MkdirAll(coverDir, 0o755); err != nil {
t.Fatalf("mkdir uploads: %v", err)
}
coverBytes := encodeJPEGBytes(t, 120, 160)
if err := os.WriteFile(filepath.Join(coverDir, "cover.jpg"), coverBytes, 0o644); err != nil {
t.Fatalf("write cover: %v", err)
}
if _, err := s.Update(doc.ID, &LibraryInput{
Slug: "dao-jia", Title: "道家研究", Author: "老子", Description: "# 介绍",
CoverURL: "/uploads/images/cover.jpg", CoverWidth: 120, CoverHeight: 160,
Published: &pub,
}); err != nil {
t.Fatalf("update cover: %v", err)
}
chapter, err := s.CreateSection(doc.ID, &SectionInput{Title: "第一章", Content: "章正文"})
if err != nil {
t.Fatalf("create chapter: %v", err)
}
if _, err := s.CreateSection(doc.ID, &SectionInput{
ParentID: &chapter.ID, Title: "小节", Content: "小节正文",
}); err != nil {
t.Fatalf("create section: %v", err)
}
f, err := s.AddFile(doc.ID, 1, "manual.epub", bytes.NewReader([]byte("EPUB-FILE-BYTES")))
if err != nil {
t.Fatalf("add file: %v", err)
}
return doc.ID, f.StoredName
}
func TestLibraryExportBook(t *testing.T) {
s, _ := newLibraryTestService(t)
uploads := t.TempDir()
s.WithUploadsDir(uploads)
docID, storedName := seedBookForExport(t, s, uploads)
exp, err := s.BuildBookExport(docID)
if err != nil {
t.Fatalf("build export: %v", err)
}
defer exp.Close()
if exp.Filename != "dao-jia.zip" {
t.Fatalf("filename = %q, want dao-jia.zip", exp.Filename)
}
zr := buildExportZip(t, exp)
raw, ok := readZipEntry(t, zr, "book.json")
if !ok {
t.Fatal("缺少 book.json")
}
var manifest libraryExportManifest
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatalf("parse book.json: %v", err)
}
if manifest.Format != libraryExportBookFormat || manifest.FormatVersion != libraryExportVersion {
t.Fatalf("清单格式异常: %s v%d", manifest.Format, manifest.FormatVersion)
}
b := manifest.Doc
if b == nil {
t.Fatal("book.json 缺少 doc")
}
if b.Title != "道家研究" || b.Slug != "dao-jia" || b.Author != "老子" || !b.Published {
t.Fatalf("元信息导出不正确: %+v", b)
}
if b.CoverFile != "cover.jpg" || b.CoverWidth != 120 || b.CoverHeight != 160 {
t.Fatalf("封面信息导出不正确: %+v", b)
}
if len(b.Sections) != 2 || b.Sections[0].Key != "s0" || b.Sections[1].ParentKey != "s0" {
t.Fatalf("章节树导出不正确: %+v", b.Sections)
}
if len(b.Files) != 1 || b.Files[0].Stored != "files/"+storedName || b.Files[0].Missing {
t.Fatalf("附件清单导出不正确: %+v", b.Files)
}
if data, ok := readZipEntry(t, zr, "files/"+storedName); !ok || string(data) != "EPUB-FILE-BYTES" {
t.Fatalf("附件内容导出不正确(ok=%v)", ok)
}
if data, ok := readZipEntry(t, zr, "cover.jpg"); !ok || !bytes.HasPrefix(data, []byte{0xff, 0xd8, 0xff}) {
t.Fatalf("封面内容导出不正确(ok=%v)", ok)
}
// 单本包不应出现全库索引
if _, ok := readZipEntry(t, zr, "library.json"); ok {
t.Fatal("单本包不应包含 library.json")
}
}
func TestLibraryExportAll(t *testing.T) {
s, _ := newLibraryTestService(t)
uploads := t.TempDir()
s.WithUploadsDir(uploads)
docID, storedName := seedBookForExport(t, s, uploads)
_ = docID
exp, err := s.BuildAllExport()
if err != nil {
t.Fatalf("build all export: %v", err)
}
defer exp.Close()
zr := buildExportZip(t, exp)
raw, ok := readZipEntry(t, zr, "library.json")
if !ok {
t.Fatal("缺少 library.json")
}
var manifest libraryExportManifest
if err := json.Unmarshal(raw, &manifest); err != nil {
t.Fatalf("parse library.json: %v", err)
}
if manifest.Format != libraryExportFormat || len(manifest.Docs) != 1 {
t.Fatalf("全库索引异常: %+v", manifest)
}
idx := manifest.Docs[0]
if idx.Dir != "docs/dao-jia/" || idx.FileCount != 1 || idx.SectionCount != 2 {
t.Fatalf("索引条目异常: %+v", idx)
}
bookRaw, ok := readZipEntry(t, zr, "docs/dao-jia/book.json")
if !ok {
t.Fatal("缺少 docs/dao-jia/book.json")
}
var book libraryExportBook
if err := json.Unmarshal(bookRaw, &book); err != nil {
t.Fatalf("parse nested book.json: %v", err)
}
if book.Slug != "dao-jia" || book.CoverFile != "cover.jpg" {
t.Fatalf("嵌套书籍清单异常: %+v", book)
}
if _, ok := readZipEntry(t, zr, "docs/dao-jia/files/"+storedName); !ok {
t.Fatal("缺少嵌套附件")
}
if _, ok := readZipEntry(t, zr, "docs/dao-jia/cover.jpg"); !ok {
t.Fatal("缺少嵌套封面")
}
}
func TestLibraryExportMissingDoc(t *testing.T) {
s, _ := newLibraryTestService(t)
if _, err := s.BuildBookExport(999); !errors.Is(err, ErrLibraryNotFound) {
t.Fatalf("不存在的条目应返回 ErrLibraryNotFound,got %v", err)
}
}