Vellum/internal/library/library_test.go
mithral 5458b0a28a Phase 2: libraries and the file scanner
- 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.
2026-10-09 16:33:56 -04:00

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))
}
}