Vellum/internal/modules/comics/comic_test.go
mithral 1b11867efe Phase 6a: directory grouping, audiobooks and music playback
A book is a folder of chapters, not a file. The scanner gained a grouping
mode so a directory of tracks becomes one item with ordered files.

- registry: MediaType.Group (file | directory). Audiobooks and music group by
  directory; everything else stays one item per file.
- scanner: builds groups, upserts one item per group, marks items missing when
  their directory disappears, and keeps the file-level added/updated/missing
  accounting. Tracks are ordered naturally, so chapter 2 precedes chapter 10.
- natsort: one shared natural-order comparator, now used by both the scanner
  and the comics module (which had its own copy).
- media: GET /items/{id}/files/{fileID} streams one specific track. It
  verifies the file belongs to the item, so an item id cannot be paired with
  another item's file id.

No new module was needed for playback: the polymorphic progress table already
keys on file_id, so per-chapter resume fell out of the existing design. The
web player resumes the exact track and offset.

Verified: 3 chapters grouped into one book titled "Mistborn", natural order,
range request returned 206, mismatched file id 404s, per-track progress
round-trips.
2026-10-09 21:20:47 -04:00

83 lines
1.9 KiB
Go

package comics
import (
"archive/zip"
"os"
"path/filepath"
"testing"
)
func writeCBZ(t *testing.T, entries map[string]string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "test.cbz")
f, err := os.Create(p)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
for name, body := range entries {
w, err := zw.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
if err := zw.Close(); err != nil {
t.Fatal(err)
}
return p
}
func TestListPagesNaturalOrder(t *testing.T) {
p := writeCBZ(t, map[string]string{
"page10.jpg": "ten",
"page2.jpg": "two",
"page1.jpg": "one",
"notes.txt": "not an image",
"__MACOSX/._page1.jpg": "junk",
})
pages, err := ListPages(p)
if err != nil {
t.Fatalf("ListPages: %v", err)
}
want := []string{"page1.jpg", "page2.jpg", "page10.jpg"}
if len(pages) != len(want) {
t.Fatalf("got %d pages, want %d: %+v", len(pages), len(want), pages)
}
for i, name := range want {
if pages[i].Name != name {
t.Errorf("page %d = %q, want %q", i+1, pages[i].Name, name)
}
if pages[i].Index != i+1 {
t.Errorf("page %d index = %d, want %d", i, pages[i].Index, i+1)
}
}
}
func TestReadPage(t *testing.T) {
p := writeCBZ(t, map[string]string{"page1.jpg": "hello-page"})
data, err := ReadPage(p, "page1.jpg")
if err != nil {
t.Fatalf("ReadPage: %v", err)
}
if string(data) != "hello-page" {
t.Errorf("page content = %q", data)
}
if _, err := ReadPage(p, "missing.jpg"); err == nil {
t.Error("ReadPage succeeded for a missing entry")
}
}
func TestListPagesRejectsArchiveWithoutImages(t *testing.T) {
p := writeCBZ(t, map[string]string{"readme.txt": "nothing to see"})
if _, err := ListPages(p); err == nil {
t.Error("ListPages succeeded on an archive with no images")
}
}