package services import ( "archive/zip" "os" "path/filepath" "testing" ) // helper to build an EPUB zip from a file map for cover tests func writeEPUB(t *testing.T, path string, files map[string]string) { t.Helper() f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() w := zip.NewWriter(f) mimetype, err := w.CreateHeader(&zip.FileHeader{Name: "mimetype", Method: zip.Store}) if err != nil { t.Fatal(err) } mimetype.Write([]byte("application/epub+zip")) for name, content := range files { fw, err := w.Create(name) if err != nil { t.Fatal(err) } if _, err := fw.Write([]byte(content)); err != nil { t.Fatal(err) } } if err := w.Close(); err != nil { t.Fatal(err) } } const containerXML = `` const tinyJPEG = "\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\x01\x00\x00\xff\xd9" // TestFindCoverInOPFAttributeOrder guards the regression where attribute // order defeated regex scraping: this OPF mirrors Grand Central's "3 Days to // Live" serialization (href before id, content before name on the meta tag). func TestFindCoverInOPFAttributeOrder(t *testing.T) { opf := ` 3 Days to Live ` files := map[string]string{ "META-INF/container.xml": containerXML, "OEBPS/package.opf": opf, "OEBPS/images/9781538752760.jpg": tinyJPEG, } epubPath := filepath.Join(t.TempDir(), "book.epub") writeEPUB(t, epubPath, files) s := NewMediaScanner(nil) coverPath, err := s.extractEPUBCover(epubPath) if err != nil { t.Fatalf("extractEPUBCover() error: %v", err) } if coverPath == "" { t.Fatal("cover not extracted - attribute order still defeats resolution") } if _, err := os.Stat(coverPath); err != nil { t.Fatalf("cover file not written: %v", err) } } // TestFindCoverInOPFCoverPage covers books that declare no raster cover at // all: the classic EPUB2/Adobe structure where cover.xhtml wraps the image // (here via SVG), reachable through the guide reference or first spine item. func TestFindCoverInOPFCoverPage(t *testing.T) { opf := ` Old Adobe Book ` coverPage := `
` files := map[string]string{ "META-INF/container.xml": containerXML, "OEBPS/package.opf": opf, "OEBPS/text/cover.xhtml": coverPage, "OEBPS/art/cover-wrap.jpg": tinyJPEG, } epubPath := filepath.Join(t.TempDir(), "adobe.epub") writeEPUB(t, epubPath, files) s := NewMediaScanner(nil) coverPath, err := s.extractEPUBCover(epubPath) if err != nil { t.Fatalf("extractEPUBCover() error: %v", err) } if coverPath == "" { t.Fatal("cover-page fallback failed to find SVG-wrapped image") } } // TestFindCoverInOPFImageFirstSpine covers store manga whose first spine // item is a raster image itself (Calibre's third resolution step). func TestFindCoverInOPFImageFirstSpine(t *testing.T) { opf := ` Manga Vol 1 ` files := map[string]string{ "META-INF/container.xml": containerXML, "OEBPS/package.opf": opf, "OEBPS/pages/0001.jpg": tinyJPEG, } epubPath := filepath.Join(t.TempDir(), "manga.epub") writeEPUB(t, epubPath, files) s := NewMediaScanner(nil) coverPath, err := s.extractEPUBCover(epubPath) if err != nil { t.Fatalf("extractEPUBCover() error: %v", err) } if coverPath == "" { t.Fatal("image-first spine cover not detected") } } // TestParseOPFContentTitleTypeAndSeries covers EPUB3 refines-based title // selection (main + subtitle joined Calibre-style) and belongs-to-collection // series with collection-type and group-position refines. func TestParseOPFContentTitleTypeAndSeries(t *testing.T) { opf := ` The Main Title The Subtitle main subtitle Programming Algorithms urn:isbn:978-3-16-148410-0 Great Series series 4.5 ` metadata, err := parseOPFContent([]byte(opf)) if err != nil { t.Fatalf("parseOPFContent() error: %v", err) } if want := "The Main Title: The Subtitle"; metadata.Title != want { t.Errorf("Title = %q, want %q", metadata.Title, want) } if metadata.Series != "Great Series" || metadata.SeriesNumber != 4 { t.Errorf("Series = %q/%d, want Great Series/4", metadata.Series, metadata.SeriesNumber) } if metadata.ISBN == "" { t.Error("urn:isbn: identifier not extracted") } if metadata.Genre != "Programming" { t.Errorf("Genre = %q, want first subject %q", metadata.Genre, "Programming") } if len(metadata.Tags) != 2 { t.Errorf("Tags = %v, want both subjects", metadata.Tags) } } // TestParseOPFContentSubjectsWithCommas verifies subject headings keep their // embedded commas as single tags (Library of Congress style headings). func TestParseOPFContentSubjectsWithCommas(t *testing.T) { opf := ` A Study in Scarlet Holmes, Sherlock (Fictitious character) -- Fiction ` metadata, err := parseOPFContent([]byte(opf)) if err != nil { t.Fatalf("parseOPFContent() error: %v", err) } if len(metadata.Tags) != 1 { t.Errorf("Tags = %v, want exactly 1 unsplit subject heading", metadata.Tags) } } // TestResolveOPFPath checks URL decoding and posix normalization of // OPF-relative hrefs. func TestResolveOPFPath(t *testing.T) { tests := []struct { opfPath, href, want string }{ {"OEBPS/package.opf", "images/cover.jpg", "OEBPS/images/cover.jpg"}, {"package.opf", "cover.jpg", "cover.jpg"}, {"OEBPS/package.opf", "../cover.jpg", "cover.jpg"}, {"OEBPS/package.opf", "my%20covers/a%20cover.jpg", "OEBPS/my covers/a cover.jpg"}, } for _, tt := range tests { if got := resolveOPFPath(tt.opfPath, tt.href); got != tt.want { t.Errorf("resolveOPFPath(%q, %q) = %q, want %q", tt.opfPath, tt.href, got, tt.want) } } }