Files
volumen/internal/biblio/biblio_test.go
T

208 lines
7.0 KiB
Go
Raw Normal View History

2026-09-18 12:03:35 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package biblio
import (
"strings"
"testing"
)
func TestParseStructuredAndRaw(t *testing.T) {
entries := Parse([]map[string]any{
{
"title": "Observational evidence from supernovae",
"authors": []any{
map[string]any{"name": "Riess, A. G.", "orcid": "0000-0002-1825-0097"},
"Filippenko, A. V.",
},
"venue": "The Astronomical Journal",
"year": "1998",
"doi": "doi:10.1103/PhysRevD.59.103502.",
},
{
"raw": "Goldhaber, G. a kol., arXiv:astro-ph/0408076, doi:10.1088/1475-7516/2004/12/010.",
},
})
if len(entries) != 2 {
t.Fatalf("entries = %d", len(entries))
}
first := entries[0]
if first.Num != 1 || first.Title == "" || len(first.Authors) != 2 {
t.Fatalf("first = %+v", first)
}
if first.Authors[0].ORCID != "0000-0002-1825-0097" {
t.Fatalf("author orcid lost: %+v", first.Authors[0])
}
if first.DOI != "10.1103/PhysRevD.59.103502" {
t.Fatalf("doi not cleaned: %q", first.DOI)
}
second := entries[1]
if second.Num != 2 || second.DOI != "10.1088/1475-7516/2004/12/010" ||
second.ArXiv != "astro-ph/0408076" {
t.Fatalf("raw entry not enriched: %+v", second)
}
}
func TestListHTMLAndPlace(t *testing.T) {
entries := Parse([]map[string]any{
{"title": "Alpha", "doi": "10.1000/alpha", "authors": []any{"A. Author"}},
})
list := ListHTML(entries)
for _, want := range []string{
`<section class="refs" id="references">`, `<li id="ref-1">`,
`href="https://doi.org/10.1000/alpha"`, "orcid", "A. Author.",
} {
if want == "orcid" {
if strings.Contains(list, want) {
t.Fatalf("unexpected orcid: %s", list)
}
continue
}
if !strings.Contains(list, want) {
t.Fatalf("list missing %q:\n%s", want, list)
}
}
// The marker is replaced in place; without it the list is appended.
body := "<p>Intro.</p>\n<p>" + Marker + "</p>\n<p>After.</p>\n"
placed := Place(body, entries)
if !strings.Contains(placed, "Intro.") || !strings.Contains(placed, "After.") ||
strings.Contains(placed, Marker) {
t.Fatalf("marker placement wrong:\n%s", placed)
}
if !strings.HasSuffix(Place("<p>Only.</p>", entries), "</section>\n") {
t.Fatal("append mode missing")
}
if Place("<p>x</p>", nil) != "<p>x</p>" {
t.Fatal("empty entries should not change the body")
}
}
func TestLinkCitationsRewritesTextOnly(t *testing.T) {
entries := Parse([]map[string]any{{"title": "A"}, {"title": "B"}})
html := "<p>Work [1] and [2], even [3] out of range.</p>" +
"<p>Code stays: <code>list[1] = 2</code>.</p>" +
"<pre>array[2]=x</pre>" +
`<a href="/x#ref-1">text [1] inside link</a>` +
`<a href="/y" data-q="[2]">href untouched</a>`
out := LinkCitations(html, entries)
if !strings.Contains(out, `<a class="ref-cite" href="#ref-1">[1]</a>`) {
t.Fatalf("inline [1] not linked:\n%s", out)
}
if strings.Contains(out, `href="#ref-3"`) {
t.Fatal("out-of-range [3] was linked")
}
if !strings.Contains(out, "<code>list[1] = 2</code>") {
t.Fatal("code content was rewritten")
}
if !strings.Contains(out, "<pre>array[2]=x</pre>") {
t.Fatal("pre content was rewritten")
}
// Inside an existing anchor the marker stays text; no nesting.
if !strings.Contains(out, ">text [1] inside link</a>") {
t.Fatalf("anchor text was rewritten:\n%s", out)
}
if !strings.Contains(out, `data-q="[2]"`) {
t.Fatal("attribute value was rewritten")
}
}
func TestStructuredEntryStripsTokensFromText(t *testing.T) {
entries := Parse([]map[string]any{{
"title": "Timescale stretch parameterization, doi:10.1088/1475-7516/2004/12/010.",
"authors": []any{"Goldhaber, G. a kol."},
}})
e := entries[0]
if e.DOI != "10.1088/1475-7516/2004/12/010" {
t.Fatalf("doi not pulled out: %q", e.DOI)
}
line := ListHTML(entries)
if got := strings.Count(line, "refs-link"); got != 1 {
t.Fatalf("expected one identifier link, got %d:\n%s", got, line)
}
if before := line[:strings.Index(line, `<a `)]; strings.Contains(before, "10.1088") {
t.Fatalf("identifier left as prose before the link:\n%s", before)
}
if !strings.Contains(line, `href="https://doi.org/10.1088/1475-7516/2004/12/010"`) {
t.Fatalf("resolver link missing:\n%s", line)
}
}
func TestCitationsJSONLDShape(t *testing.T) {
entries := Parse([]map[string]any{
{
"title": "Alpha",
"authors": []any{map[string]any{"name": "A. Author", "orcid": "0000-0002-1825-0097"}},
"venue": "Journal", "year": "2020", "doi": "10.1000/a",
},
})
cits := Citations(entries)
if len(cits) != 1 {
t.Fatalf("citations = %v", cits)
}
c := cits[0]
if c["@type"] != "ScholarlyArticle" || c["identifier"] != "https://doi.org/10.1000/a" {
t.Fatalf("citation shape wrong: %v", c)
}
persons, ok := c["author"].([]map[string]any)
if !ok || persons[0]["identifier"] != "https://orcid.org/0000-0002-1825-0097" {
t.Fatalf("author orcid missing: %v", c["author"])
}
}
func TestInternalCrossLinks(t *testing.T) {
entries := Parse([]map[string]any{
{"num": int64(1), "title": "Teorie deterministického substrátu", "doi": "10.5555/tdssc.2026"},
{"num": int64(2), "raw": "Balvín, P.: TDSSC, viz doi:10.5555/tdssc.2026; externí doi:10.9999/other (2020)."},
{"num": int64(3), "title": "Externí práce", "doi": "10.9999/other"},
{"num": int64(4), "title": "Bez identifikátoru"},
})
entries[0].Internal = "/api/volumen/posts/tdssc"
entries[1].Internal = "/api/volumen/posts/tdssc"
list := ListHTML(entries)
for _, want := range []string{
`href="/api/volumen/posts/tdssc">doi:10.5555/tdssc.2026<`,
`href="https://doi.org/10.9999/other">doi:10.9999/other<`,
} {
if !strings.Contains(list, want) {
t.Fatalf("list missing %q:\n%s", want, list)
}
}
if strings.Count(list, `href="/api/volumen/posts/tdssc"`) != 2 {
t.Fatalf("internal link count wrong:\n%s", list)
}
cits := Citations(entries)
if cits[0]["url"] != "/api/volumen/posts/tdssc" {
t.Fatalf("citation url not internal: %v", cits[0])
}
// The canonical identifier survives unchanged next to the internal link.
if cits[0]["identifier"] != "https://doi.org/10.5555/tdssc.2026" {
t.Fatalf("citation identifier changed: %v", cits[0])
}
if _, ok := cits[2]["url"]; ok {
t.Fatalf("external citation gained a url: %v", cits[2])
}
if _, ok := cits[3]["identifier"]; ok {
t.Fatalf("bare citation gained an identifier: %v", cits[3])
}
}
// A raw entry's internal link is escaped like every other href: the slug
// it names comes from the store, and a hand-edited file could carry a
// quote that must not open an attribute in the rendered list.
func TestRawEntryEscapesInternalLink(t *testing.T) {
entries := Parse([]map[string]any{
{"raw": "Viz doi:10.5555/hostile.2026 (2020).", "doi": "10.5555/hostile.2026"},
})
entries[0].Internal = `/api/volumen/posts/x" onmouseover="alert(1)`
out := entries[0].identifierLinks()
if strings.Contains(out, `" onmouseover=`) {
t.Fatalf("the internal link broke out of its attribute: %q", out)
}
if !strings.Contains(out, "href=\"/api/volumen/posts/x&#34; onmouseover=&#34;alert(1)\"") {
t.Fatalf("the internal link lost its target: %q", out)
}
}