// Package chunktest builds the on-disk source fixtures the ingest path reads. It lives in its own // package because two packages need the SAME epub shape: the ingest unit tests and the runner's // end-to-end tests. Duplicating a zip/OPF builder in both would let the two drift apart, and the // whole point of the e2e fixture is that it is the file the unit tests already pin. // // The CONTAINER layer (mimetype first and stored, container.xml, the entry writes) is not built here // any more: it is internal/bookfile's, the same code the engine's book writer ships an EPUB through, so // the fixture the reader is pinned against and the file the writer produces cannot drift apart at the // zip level. What stays here is deliberately the TEST half — the bare OPF stub, the decoy /<style> // in every chapter head that ingest_test proves are dropped, the MType/EntryName knobs for malformed // manifests — none of which may ever reach a reader. package chunktest import ( "os" "path/filepath" "strings" "testing" "textmachine/backend/internal/bookfile" ) type Chapter struct { ID string Href string // relative to the OPF dir (OEBPS/), as the MANIFEST spells it MType string // "" → application/xhtml+xml Body string // inner xhtml of <body> // EntryName is the zip entry the file is actually written to (relative to the OPF dir), // for fixtures whose manifest href does NOT spell the entry name: a percent-encoded href // or one carrying a #fragment. "" → the href itself, so every existing fixture is unchanged. EntryName string } // entryName is where this chapter's bytes are written inside OEBPS/. func (c Chapter) entryName() string { if c.EntryName != "" { return c.EntryName } return c.Href } // BuildEPUB writes a minimal but real epub (container.xml → OPF → spine → xhtml) // to a temp .epub file and returns its path. spineIDs gives the reading order // (may differ from manifest order to prove the spine drives it). func BuildEPUB(t *testing.T, chapters []Chapter, spineIDs []string) string { t.Helper() path := filepath.Join(t.TempDir(), "book.epub") BuildEPUBAt(t, path, chapters, spineIDs) return path } // BuildEPUBAt writes the epub to a caller-chosen path (used by the runner e2e). func BuildEPUBAt(t *testing.T, path string, chapters []Chapter, spineIDs []string) { t.Helper() f, err := os.Create(path) if err != nil { t.Fatal(err) } defer f.Close() // OCF requires "mimetype" first in the archive and STORED, not deflated, and container.xml pointing // at the OPF. The production container writes both (bookfile.NewContainer); the fixture rides it so // it stays the real epub shape the reader will meet, by construction rather than by copy. c, err := bookfile.NewContainer(f, "OEBPS/content.opf") if err != nil { t.Fatal(err) } add := func(name, content string) { if err := c.Add(name, []byte(content)); err != nil { t.Fatal(err) } } var manifest, spine strings.Builder for _, c := range chapters { mt := c.MType if mt == "" { mt = "application/xhtml+xml" } manifest.WriteString(`<item id="` + c.ID + `" href="` + c.Href + `" media-type="` + mt + `"/>` + "\n") } for _, id := range spineIDs { spine.WriteString(`<itemref idref="` + id + `"/>` + "\n") } add("OEBPS/content.opf", `<?xml version="1.0" encoding="utf-8"?> <package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="uid"> <metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:title>Test</dc:title></metadata> <manifest>`+manifest.String()+`</manifest> <spine>`+spine.String()+`</spine> </package>`) for _, c := range chapters { // The FILE is (x)html when its ENTRY name says so, regardless of how the manifest // media-type labels it — real epubs mislabel xhtml as application/xml, etc. The entry // name (not the href) decides, because an href may be percent-encoded or fragment-bearing. entry := c.entryName() switch strings.ToLower(filepath.Ext(entry)) { case ".xhtml", ".html", ".htm", ".xml": add("OEBPS/"+entry, `<?xml version="1.0" encoding="utf-8"?> <html xmlns="http://www.w3.org/1999/xhtml"><head><title>c `+c.Body+``) default: add("OEBPS/"+entry, c.Body) // non-xhtml asset (e.g. image bytes stand-in) } } if err := c.Close(); err != nil { t.Fatal(err) } }