package build import ( "bytes" "image" imgjpeg "image/jpeg" imgpng "image/png" "net/url" "os" "path/filepath" "regexp" "strings" "testing" "time" "golang.org/x/net/html" "git.monoexp.com/bajdev/rekopub/internal/assets" "git.monoexp.com/bajdev/rekopub/internal/clean" "git.monoexp.com/bajdev/rekopub/internal/epubtest" "git.monoexp.com/bajdev/rekopub/internal/extract" ) func testArticle(t *testing.T, body string) *extract.Article { t.Helper() doc, err := html.Parse(strings.NewReader("
" + body + "")) if err != nil { t.Fatal(err) } return &extract.Article{ URL: "https://example.com/posts/1?a=1&b=2", Title: "Tom & Jerry <3", Author: "Ada Lovelace", SiteName: "Example News", Description: "A test article.", Language: "en", Published: time.Date(2025, 3, 14, 9, 30, 0, 0, time.UTC), Content: doc.FirstChild.LastChild, // html > body } } func buildBytes(t *testing.T, a *extract.Article) []byte { t.Helper() base, err := url.Parse(a.URL) if err != nil { t.Fatal(err) } body, err := clean.Content(a.Content, base) if err != nil { t.Fatal(err) } data, err := Build(a, body, nil, nil) if err != nil { t.Fatal(err) } return data } func TestBuild(t *testing.T) { a := testArticle(t, `First & second.
Line two.
text
") first := id.FindString(epubtest.Open(t, buildBytes(t, a)).File(t, ".opf")) second := id.FindString(epubtest.Open(t, buildBytes(t, a)).File(t, ".opf")) if first == "" || first != second { t.Errorf("identifiers %q and %q, want the same non-empty identifier", first, second) } a.URL = "https://example.com/other" if other := id.FindString(epubtest.Open(t, buildBytes(t, a)).File(t, ".opf")); other == first { t.Errorf("different URLs gave the same identifier %q", other) } } func TestByline(t *testing.T) { date := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC) tests := []struct { author, site string date time.Time want string }{ {"A", "S", date, "A · S · 2 January 2025"}, {"", "S", date, "S · 2 January 2025"}, {"A", "", time.Time{}, "A"}, {"", "", time.Time{}, ""}, } for _, tt := range tests { got := byline(&extract.Article{Author: tt.author, SiteName: tt.site, Published: tt.date}) if got != tt.want { t.Errorf("byline(%q, %q, %v) = %q, want %q", tt.author, tt.site, tt.date, got, tt.want) } } } func TestChapterWithoutByline(t *testing.T) { a := testArticle(t, "text
") a.Author, a.SiteName, a.Published = "", "", time.Time{} chapter := epubtest.Open(t, buildBytes(t, a)).File(t, "chapter.xhtml") if strings.Contains(chapter, "byline") { t.Errorf("chapter has a byline paragraph:\n%s", chapter) } } func TestBuildWithImages(t *testing.T) { var png bytes.Buffer img := image.NewGray(image.Rect(0, 0, 40, 30)) if err := imgpng.Encode(&png, img); err != nil { t.Fatal(err) } a := testArticle(t, "") body, err := clean.Content(a.Content, mustURL(t, a.URL)) if err != nil { t.Fatal(err) } // As left by the assets package: src is the image's filename. The second // image wasn't processed and must not end up in the book. frag, err := html.ParseFragment(strings.NewReader(`
x 
`) {
t.Errorf("chapter doesn't reference the stored image:\n%s", chapter)
}
if strings.Contains(chapter, "remote.example") {
t.Errorf("chapter references a remote image:\n%s", chapter)
}
if opf := book.File(t, ".opf"); !strings.Contains(opf, `href="images/abc123.png"`) || !strings.Contains(opf, `media-type="image/png"`) {
t.Errorf("OPF manifest is missing the image:\n%s", opf)
}
path := filepath.Join(t.TempDir(), "book.epub")
if err := os.WriteFile(path, data, 0o644); err != nil {
t.Fatal(err)
}
epubtest.EPUBCheck(t, path)
}
func mustURL(t *testing.T, raw string) *url.URL {
t.Helper()
u, err := url.Parse(raw)
if err != nil {
t.Fatal(err)
}
return u
}
func TestBuildMetadataCoverAndContents(t *testing.T) {
a := testArticle(t, `Intro.
a
b
c
Just text.
") a.SiteName, a.Published = "", time.Time{} book := epubtest.Open(t, buildBytes(t, a)) opf := book.File(t, ".opf") if strings.Contains(opf, "dc:publisher") || strings.Contains(opf, "dc:date") { t.Errorf("OPF has empty publisher or date:\n%s", opf) } if !strings.Contains(book.File(t, "nav.xhtml"), `