143 lines
4.2 KiB
Go
143 lines
4.2 KiB
Go
package scanner
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"booklib/internal/bookfile"
|
|
"booklib/internal/config"
|
|
"booklib/internal/db"
|
|
"booklib/internal/redispkg"
|
|
"booklib/internal/store"
|
|
)
|
|
|
|
// 返回 scanner、library、解析过符号链接的 root、以及一个满足 progress FK 的 uid
|
|
func setupLib(t *testing.T) (*Scanner, store.Library, string, int64) {
|
|
t.Helper()
|
|
url := os.Getenv("DATABASE_URL")
|
|
if url == "" {
|
|
t.Skip("DATABASE_URL not set")
|
|
}
|
|
ctx := context.Background()
|
|
p, _ := db.Connect(ctx, url)
|
|
db.Migrate(ctx, p)
|
|
st := store.New(p)
|
|
p.Exec(ctx, "DELETE FROM reading_progress; DELETE FROM books; DELETE FROM libraries; DELETE FROM users")
|
|
books := t.TempDir()
|
|
cache := t.TempDir()
|
|
resolved := mustResolve(t, books)
|
|
libID, err := st.CreateLibrary(ctx, "t", filepath.Join(resolved, "lib"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
uid, err := st.CreateUser(ctx, "scantest", "h", "member")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
root := filepath.Join(resolved, "lib")
|
|
os.MkdirAll(filepath.Join(root, "series-a"), 0o755)
|
|
cfg := &config.Config{BooksDir: resolved, CacheDir: cache, ScanInterval: time.Minute}
|
|
lib, _ := st.GetLibrary(ctx, libID)
|
|
return New(st, cfg, redispkg.New(os.Getenv("REDIS_URL"))), lib, root, uid
|
|
}
|
|
|
|
func mustResolve(t *testing.T, p string) string {
|
|
r, err := filepath.EvalSymlinks(p) // macOS 上 t.TempDir 是 /var→/private 符号链接
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func writeCBZ(t *testing.T, path string, pages int) {
|
|
t.Helper()
|
|
os.MkdirAll(filepath.Dir(path), 0o755)
|
|
buf := &bytes.Buffer{}
|
|
zw := zip.NewWriter(buf)
|
|
for i := 1; i <= pages; i++ {
|
|
w, _ := zw.Create(fmt.Sprintf("%02d.jpg", i))
|
|
w.Write(bytes.Repeat([]byte("JPG"), 100))
|
|
}
|
|
zw.Close()
|
|
os.WriteFile(path, buf.Bytes(), 0o644)
|
|
}
|
|
|
|
func TestScanFullLifecycle(t *testing.T) {
|
|
sc, lib, root, uid := setupLib(t)
|
|
ctx := context.Background()
|
|
writeCBZ(t, filepath.Join(root, "series-a", "vol_01.cbz"), 3)
|
|
os.WriteFile(filepath.Join(root, "notes.txt"), []byte("hi"), 0o644)
|
|
sc.ScanLibrary(ctx, lib)
|
|
|
|
meta, _ := sc.st.ListBookMeta(ctx, lib.ID)
|
|
if len(meta) != 2 {
|
|
t.Fatalf("want 2 books got %v", meta)
|
|
}
|
|
b, err := sc.st.GetBook(ctx, meta["series-a/vol_01.cbz"].ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if b.Format != "cbz" || b.PageCount != 3 || b.Title != "vol 01" {
|
|
t.Fatalf("bad book %+v", b)
|
|
}
|
|
key := bookfile.DirKey(b.ID, bookfile.Hash(b.FileSize, b.ModTS))
|
|
covers, _ := os.ReadDir(bookfile.CoverDir(sc.cfg.CacheDir, key))
|
|
if len(covers) == 0 {
|
|
t.Fatal("cover not built")
|
|
}
|
|
// 二次扫描:无变化 → 不动
|
|
before := b.AddedAt
|
|
sc.ScanLibrary(ctx, lib)
|
|
b2, _ := sc.st.GetBook(ctx, b.ID)
|
|
if !b2.AddedAt.Equal(before) || b2.FileSize != b.FileSize {
|
|
t.Fatal("unchanged file must be untouched")
|
|
}
|
|
// 修改:page_count 变、hash 变、旧缓存被扫尾清掉
|
|
writeCBZ(t, filepath.Join(root, "series-a", "vol_01.cbz"), 5)
|
|
os.Chtimes(filepath.Join(root, "series-a", "vol_01.cbz"), time.Now(), time.Now())
|
|
sc.ScanLibrary(ctx, lib)
|
|
b3, _ := sc.st.GetBook(ctx, b.ID)
|
|
if b3.PageCount != 5 {
|
|
t.Fatalf("not updated: %+v", b3)
|
|
}
|
|
if _, err := os.Stat(bookfile.CoverDir(sc.cfg.CacheDir, key)); !os.IsNotExist(err) {
|
|
t.Fatal("stale cover cache remains")
|
|
}
|
|
// 删除:行没了,进度按 path 还在(spec §4)
|
|
if err := sc.st.UpsertProgress(ctx, uid, lib.ID, "notes.txt", []byte("{}"), 0.5); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Remove(filepath.Join(root, "notes.txt")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sc.ScanLibrary(ctx, lib)
|
|
if _, err := sc.st.GetBook(ctx, b.ID); err != nil {
|
|
t.Fatal("cbz vanished wrongly")
|
|
}
|
|
meta2, _ := sc.st.ListBookMeta(ctx, lib.ID)
|
|
if _, ok := meta2["notes.txt"]; ok {
|
|
t.Fatal("deleted file still in db")
|
|
}
|
|
if pr, err := sc.st.GetProgress(ctx, uid, lib.ID, "notes.txt"); err != nil || pr.Percent != 0.5 {
|
|
t.Fatalf("progress must survive book removal: %+v %v", pr, err)
|
|
}
|
|
}
|
|
|
|
func TestScanBrokenCBZStateError(t *testing.T) {
|
|
sc, lib, root, _ := setupLib(t)
|
|
ctx := context.Background()
|
|
os.WriteFile(filepath.Join(root, "bad.cbz"), []byte("not a zip"), 0o644)
|
|
sc.ScanLibrary(ctx, lib)
|
|
meta, _ := sc.st.ListBookMeta(ctx, lib.ID)
|
|
b, _ := sc.st.GetBook(ctx, meta["bad.cbz"].ID)
|
|
if b.State != "error" || b.ErrMsg == "" {
|
|
t.Fatalf("want error state, got %+v", b)
|
|
}
|
|
}
|