package ebooks
import (
"archive/zip"
"encoding/xml"
"os"
"path/filepath"
"testing"
)
const testOPF = `
The Test Book
Ada Lovelace
Charles Babbage
en
urn:uuid:abc
Vellum Press
2026-01-01
`
const testContainer = `
`
func writeEPUB(t *testing.T, opf, container string) string {
t.Helper()
p := filepath.Join(t.TempDir(), "test.epub")
f, err := os.Create(p)
if err != nil {
t.Fatal(err)
}
defer f.Close()
zw := zip.NewWriter(f)
add := func(name, body string) {
w, err := zw.Create(name)
if err != nil {
t.Fatal(err)
}
if _, err := w.Write([]byte(body)); err != nil {
t.Fatal(err)
}
}
add("mimetype", "application/epub+zip")
add("META-INF/container.xml", container)
add("OEBPS/content.opf", opf)
add("OEBPS/images/cover.png", "not-a-real-png")
add("OEBPS/chapter1.xhtml", "
")
if err := zw.Close(); err != nil {
t.Fatal(err)
}
return p
}
func TestParseMetadata(t *testing.T) {
md, err := Parse(writeEPUB(t, testOPF, testContainer))
if err != nil {
t.Fatalf("Parse: %v", err)
}
if md.Title != "The Test Book" {
t.Errorf("Title = %q", md.Title)
}
if len(md.Authors) != 2 || md.Authors[0] != "Ada Lovelace" {
t.Errorf("Authors = %v", md.Authors)
}
if md.Language != "en" || md.Publisher != "Vellum Press" || md.Date != "2026-01-01" {
t.Errorf("metadata = %+v", md)
}
if md.SpineLength != 2 {
t.Errorf("SpineLength = %d, want 2", md.SpineLength)
}
// CoverHref must be resolved relative to the OPF directory.
if md.CoverHref != "OEBPS/images/cover.png" {
t.Errorf("CoverHref = %q, want OEBPS/images/cover.png", md.CoverHref)
}
}
func TestCoverHrefFallbacks(t *testing.T) {
// No properties="cover-image"; rely on .
opfMeta := `x
`
if got := coverHref(mustParseOPF(t, opfMeta)); got != "c.jpg" {
t.Errorf("meta cover fallback = %q, want c.jpg", got)
}
// No marker at all; fall back to an image whose href mentions cover.
opfPlain := `x
`
if got := coverHref(mustParseOPF(t, opfPlain)); got != "images/my-cover.png" {
t.Errorf("href heuristics = %q, want images/my-cover.png", got)
}
// No candidate at all.
opfNone := `x
`
if got := coverHref(mustParseOPF(t, opfNone)); got != "" {
t.Errorf("no-cover case = %q, want empty", got)
}
}
func TestJoinOPFRefusesEscape(t *testing.T) {
if got := joinOPF("OEBPS", "../../etc/passwd"); got != "" {
t.Errorf("joinOPF escaped the archive: %q", got)
}
if got := joinOPF("OEBPS", "images/c.png"); got != "OEBPS/images/c.png" {
t.Errorf("joinOPF = %q", got)
}
}
func TestParseRejectsNonEPUB(t *testing.T) {
p := filepath.Join(t.TempDir(), "not.epub")
if err := os.WriteFile(p, []byte("this is not a zip"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := Parse(p); err == nil {
t.Error("Parse succeeded on a non-zip file")
}
}
func mustParseOPF(t *testing.T, s string) opfXML {
t.Helper()
var o opfXML
if err := xml.Unmarshal([]byte(s), &o); err != nil {
t.Fatal(err)
}
return o
}