Vellum/internal/library/scan_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

130 lines
3.6 KiB
Go

package library
import (
"context"
"os"
"path/filepath"
"testing"
)
func TestScanLifecycle(t *testing.T) {
db := testDB(t)
ctx := context.Background()
root := t.TempDir()
writeFile(t, filepath.Join(root, "a.epub"), "aaaaaaaaaa")
writeFile(t, filepath.Join(root, "b.epub"), "bbbbbbbbbbbbbbbbbbbb")
writeFile(t, filepath.Join(root, "sub", "c.epub"), "cccccccccccccccccccccccccccccc")
writeFile(t, filepath.Join(root, "notes.txt"), "not media")
writeFile(t, filepath.Join(root, ".hidden.epub"), "hidden")
writeFile(t, filepath.Join(root, ".git", "d.epub"), "in a hidden dir")
lib, err := Create(ctx, db, "Ebooks", "ebooks", root)
if err != nil {
t.Fatalf("Create: %v", err)
}
// First scan: three supported files, nothing else.
res, err := Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatalf("Scan: %v", err)
}
if res.Added != 3 || res.Scanned != 3 {
t.Fatalf("first scan added=%d scanned=%d, want 3/3 (%+v)", res.Added, res.Scanned, res)
}
if n, _ := CountItems(ctx, db, lib.ID); n != 3 {
t.Errorf("item count = %d, want 3", n)
}
// Second scan: nothing changed.
res, err = Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatal(err)
}
if res.Unchanged != 3 || res.Added != 0 || res.Updated != 0 {
t.Errorf("second scan unchanged=%d added=%d updated=%d, want 3/0/0", res.Unchanged, res.Added, res.Updated)
}
// Modify a.epub (different size).
writeFile(t, filepath.Join(root, "a.epub"), "aaaaaaaaaaaaaaaaaaaaaaaaaaaa")
res, err = Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatal(err)
}
if res.Updated != 1 || res.Unchanged != 2 {
t.Errorf("after modify updated=%d unchanged=%d, want 1/2", res.Updated, res.Unchanged)
}
// Delete b.epub: it goes missing, it is not deleted.
if err := os.Remove(filepath.Join(root, "b.epub")); err != nil {
t.Fatal(err)
}
res, err = Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatal(err)
}
if res.Missing != 1 {
t.Errorf("after delete missing=%d, want 1", res.Missing)
}
if n, _ := CountItems(ctx, db, lib.ID); n != 2 {
t.Errorf("item count (excluding missing) = %d, want 2", n)
}
// The row still exists, flagged missing.
var missing int
if err := db.QueryRowContext(ctx,
`SELECT missing FROM media_items WHERE library_id = ? AND path = ?`, lib.ID, "b.epub").Scan(&missing); err != nil {
t.Fatalf("missing item row was deleted: %v", err)
}
if missing != 1 {
t.Errorf("missing flag = %d, want 1", missing)
}
// Restore b.epub.
writeFile(t, filepath.Join(root, "b.epub"), "bbbbbbbbbbbbbbbbbbbb")
res, err = Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatal(err)
}
if res.Restored != 1 {
t.Errorf("after restore restored=%d, want 1 (%+v)", res.Restored, res)
}
if n, _ := CountItems(ctx, db, lib.ID); n != 3 {
t.Errorf("item count after restore = %d, want 3", n)
}
}
func TestScanUnreadableRoot(t *testing.T) {
db := testDB(t)
ctx := context.Background()
root := t.TempDir()
writeFile(t, filepath.Join(root, "a.epub"), "x")
lib, err := Create(ctx, db, "Ebooks", "ebooks", root)
if err != nil {
t.Fatal(err)
}
// Index it once so there is a known file to go missing.
if _, err := Scan(ctx, db, lib, testLogger()); err != nil {
t.Fatal(err)
}
// Remove the root out from under the library.
if err := os.RemoveAll(root); err != nil {
t.Fatal(err)
}
res, err := Scan(ctx, db, lib, testLogger())
if err != nil {
t.Fatalf("scan should not fail on a missing root: %v", err)
}
if res.Added != 0 {
t.Errorf("added=%d, want 0", res.Added)
}
if res.Missing != 1 {
t.Errorf("missing=%d, want 1 (existing item now vanished)", res.Missing)
}
if len(res.Errors) == 0 {
t.Error("expected the walk error to be recorded")
}
}