- library: CRUD with root-path resolution (absolute, symlink-free, must be readable). A library name is unique; the root is validated at creation. - registry: media types declare their extensions; the core never imports a module. RegisterType is the extension point. - scanner: walks a root, reconciles media_items/media_files in one transaction, skips unchanged files by size+mtime, and marks vanished files missing rather than deleting them (progress is the one thing here that cannot be regenerated). A single unreadable file never fails a scan. - migration 0002: `missing` flags on items and files, plus indexes. - endpoints: media-types, library list/get/create/delete, per-library items, and scan. Library reads go through the same grant chokepoint as everything else; an unauthorised library 404s rather than 403s so its existence is not leaked.
111 lines
3 KiB
Go
111 lines
3 KiB
Go
package library
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/mithral/vellum/internal/core"
|
|
"github.com/mithral/vellum/migrations"
|
|
)
|
|
|
|
func testDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
db, err := core.OpenDB(filepath.Join(t.TempDir(), "lib.db"))
|
|
if err != nil {
|
|
t.Fatalf("OpenDB: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
log := slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
if err := core.Migrate(context.Background(), db, migrations.FS, log); err != nil {
|
|
t.Fatalf("Migrate: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func testLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
|
|
func writeFile(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestRegistryBasics(t *testing.T) {
|
|
if err := ValidateKind("ebooks"); err != nil {
|
|
t.Errorf("ebooks should be valid: %v", err)
|
|
}
|
|
if err := ValidateKind("nonsense"); !errors.Is(err, ErrUnknownKind) {
|
|
t.Errorf("nonsense kind error = %v, want ErrUnknownKind", err)
|
|
}
|
|
if k, ok := KindForExtension(".epub"); !ok || k != "ebooks" {
|
|
t.Errorf("KindForExtension(.epub) = %q,%v", k, ok)
|
|
}
|
|
if k, ok := KindForExtension("CBZ"); !ok || k != "comics" {
|
|
t.Errorf("KindForExtension(CBZ) = %q,%v (should be case-insensitive)", k, ok)
|
|
}
|
|
if len(SupportedTypes()) == 0 {
|
|
t.Error("no media types registered")
|
|
}
|
|
}
|
|
|
|
func TestCleanTitle(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Project Hail Mary.epub": "Project Hail Mary",
|
|
"the_hobbit.mobi": "the hobbit",
|
|
"Book.Title.2020.epub": "Book Title 2020",
|
|
"/deep/path/Saga 001.cbz": "Saga 001",
|
|
}
|
|
for in, want := range cases {
|
|
if got := CleanTitle(in); got != want {
|
|
t.Errorf("CleanTitle(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreateLibraryValidation(t *testing.T) {
|
|
db := testDB(t)
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
|
|
if _, err := Create(ctx, db, "Good", "ebooks", root); err != nil {
|
|
t.Fatalf("valid create failed: %v", err)
|
|
}
|
|
if _, err := Create(ctx, db, "Good", "ebooks", root); !errors.Is(err, ErrNameExists) {
|
|
t.Errorf("duplicate name error = %v, want ErrNameExists", err)
|
|
}
|
|
if _, err := Create(ctx, db, "Bad", "nonsense", root); !errors.Is(err, ErrUnknownKind) {
|
|
t.Errorf("bad kind error = %v, want ErrUnknownKind", err)
|
|
}
|
|
if _, err := Create(ctx, db, "Bad", "ebooks", "/nope/not/here"); !errors.Is(err, ErrInvalidRoot) {
|
|
t.Errorf("bad root error = %v, want ErrInvalidRoot", err)
|
|
}
|
|
}
|
|
|
|
func TestListVisibleNeverMeansAll(t *testing.T) {
|
|
db := testDB(t)
|
|
ctx := context.Background()
|
|
root := t.TempDir()
|
|
if _, err := Create(ctx, db, "Ebooks", "ebooks", root); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
libs, err := ListVisible(ctx, db, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(libs) != 0 {
|
|
t.Errorf("ListVisible(nil) returned %d libraries; empty set must mean none", len(libs))
|
|
}
|
|
}
|