Files

232 lines
7.3 KiB
Go
Raw Permalink 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 payloads
import (
"strings"
"testing"
"sourcedock.dev/petrbalvin/volumen/internal/frontmatter"
"sourcedock.dev/petrbalvin/volumen/internal/post"
)
const refsPost = `+++
title = "Cite"
slug = "cite"
date = 2026-08-18
[[refs]]
title = "Observational evidence from supernovae"
authors = [{ name = "Riess, A. G.", orcid = "0000-0002-1825-0097" }]
venue = "The Astronomical Journal"
year = 1998
doi = "10.1103/PhysRevD.59.103502"
+++
Tvrzení [1].
`
func TestDetailCarriesReferences(t *testing.T) {
p := parsePost(t, refsPost)
d, err := BuildDetail(p, "https://example.com")
if err != nil {
t.Fatalf("BuildDetail: %v", err)
}
if len(d.References) != 1 {
t.Fatalf("references = %d", len(d.References))
}
ref := d.References[0]
if ref.Title == "" || ref.DOI != "10.1103/PhysRevD.59.103502" ||
ref.Authors[0].ORCID != "0000-0002-1825-0097" {
t.Fatalf("reference = %+v", ref)
}
// The JSON-LD block cites the work back with author and identifier.
for _, want := range []string{
`"citation"`, `"ScholarlyArticle"`,
`https://doi.org/10.1103/PhysRevD.59.103502`,
`https://orcid.org/0000-0002-1825-0097`,
} {
if !strings.Contains(d.Meta.JSONLD, want) {
t.Fatalf("json-ld missing %q:\n%s", want, d.Meta.JSONLD)
}
}
// The HTML carries the linked citation too.
if !strings.Contains(d.HTML, `href="#ref-1"`) {
t.Fatalf("html missing the citation anchor:\n%s", d.HTML)
}
}
// A post without refs omits the member entirely: the wire shape of
// every ordinary post is unchanged.
func TestDetailOmitsEmptyReferences(t *testing.T) {
p := parsePost(t, "+++\ntitle = \"Plain\"\nslug = \"plain\"\n+++\nx\n")
d, err := BuildDetail(p, "https://example.com")
if err != nil {
t.Fatalf("BuildDetail: %v", err)
}
if len(d.References) != 0 {
t.Fatalf("references = %v", d.References)
}
if strings.Contains(d.Meta.JSONLD, "citation") {
t.Fatalf("plain post gained citations:\n%s", d.Meta.JSONLD)
}
}
// When the cited work is published in the same instance, the detail
// exposes the internal link in every shape the consumer reads: the
// rendered HTML, the references member, and the JSON-LD citation.
func TestReferencesCarryInternalLink(t *testing.T) {
p := parsePost(t, refsPost)
p.SetLinkIndex(map[string]string{"10.1103/physrevd.59.103502": "riess-1998"})
d, err := BuildDetail(p, "https://example.com")
if err != nil {
t.Fatalf("BuildDetail: %v", err)
}
if d.References[0].Internal != "/api/volumen/posts/riess-1998" {
t.Fatalf("references internal = %+v", d.References[0])
}
if !strings.Contains(d.HTML, `href="/api/volumen/posts/riess-1998"`) {
t.Fatalf("html missing the internal link:\n%s", d.HTML)
}
if !strings.Contains(d.Meta.JSONLD, "/api/volumen/posts/riess-1998") {
t.Fatalf("json-ld missing the internal url:\n%s", d.Meta.JSONLD)
}
// The canonical DOI identifier stays in the JSON-LD beside it.
if !strings.Contains(d.Meta.JSONLD, "https://doi.org/10.1103/PhysRevD.59.103502") {
t.Fatalf("canonical identifier lost:\n%s", d.Meta.JSONLD)
}
}
func existingWithRefs(t *testing.T) *post.Post {
t.Helper()
m := frontmatter.NewMeta()
m.Set("title", "Hello")
m.Set("slug", "hello")
// The shape a parsed file carries: an array of tables.
m.Set("refs", []map[string]any{
{"raw": "Riess, A. G. et al. 1998, AJ 116, 1009"},
})
return post.New(m, "body")
}
func refTables(t *testing.T, p *post.Post) []map[string]any {
t.Helper()
raw, ok := p.Metadata.Get("refs")
if !ok {
return nil
}
list, ok := raw.([]map[string]any)
if !ok {
t.Fatalf("refs = %T, want []map[string]any", raw)
}
return list
}
// A form that carries no refs field leaves the stored list untouched: a
// save from a page without its bibliography card still round-trips the
// file as it stands.
func TestRefsAbsentFromFormKeepsTheStoredList(t *testing.T) {
p, err := PostFromParams(map[string]string{
"title": "Hello", "slug": "hello", "body": "new",
}, existingWithRefs(t))
if err != nil {
t.Fatalf("PostFromParams: %v", err)
}
tables := refTables(t, p)
if len(tables) != 1 || tables[0]["raw"] != "Riess, A. G. et al. 1998, AJ 116, 1009" {
t.Fatalf("refs = %v", tables)
}
}
// An empty list is an explicit deletion, not a lost field.
func TestRefsEmptyListDeletesTheKey(t *testing.T) {
p, err := PostFromParams(map[string]string{
"title": "Hello", "slug": "hello", "body": "b", "refs": "[]",
}, existingWithRefs(t))
if err != nil {
t.Fatalf("PostFromParams: %v", err)
}
if _, ok := p.Metadata.Get("refs"); ok {
t.Fatal("refs survived an explicit empty list")
}
}
// The editor's JSON becomes the frontmatter tables: values trimmed,
// identifiers normalised, author ORCIDs kept as name tables, an explicit
// number preserved, a blank row dropped.
func TestRefsFormWritesCanonicalTables(t *testing.T) {
p, err := PostFromParams(map[string]string{
"title": "Hello", "slug": "hello", "body": "b",
"refs": `[
{"raw": " Trimmed raw line. "},
{},
{"num": 2,
"authors": [{"name": "Adam Riess", "orcid": "0000-0002-1825-0097"}, "plain author"],
"title": "Observational Evidence from Supernovae",
"venue": "The Astronomical Journal", "year": "1998",
"volume": "116", "pages": "1009",
"doi": "https://doi.org/10.1086/300499",
"arxiv": "arXiv:astro-ph/9805201",
"url": "https://example.com/paper"}
]`,
}, existingWithRefs(t))
if err != nil {
t.Fatalf("PostFromParams: %v", err)
}
tables := refTables(t, p)
if len(tables) != 2 {
t.Fatalf("tables = %v", tables)
}
if tables[0]["raw"] != "Trimmed raw line." {
t.Fatalf("raw = %q", tables[0]["raw"])
}
second := tables[1]
if second["num"] != int64(2) {
t.Fatalf("num = %v", second["num"])
}
if second["doi"] != "10.1086/300499" {
t.Fatalf("doi = %q", second["doi"])
}
if second["arxiv"] != "astro-ph/9805201" {
t.Fatalf("arxiv = %q", second["arxiv"])
}
authors, ok := second["authors"].([]any)
if !ok || len(authors) != 2 {
t.Fatalf("authors = %v", second["authors"])
}
first, ok := authors[0].(map[string]any)
if !ok || first["name"] != "Adam Riess" || first["orcid"] != "0000-0002-1825-0097" {
t.Fatalf("author = %v", authors[0])
}
if authors[1] != "plain author" {
t.Fatalf("second author = %v", authors[1])
}
}
func TestRefsFormRejectsBadPayloads(t *testing.T) {
cases := []struct {
name string
refs string
want string
}{
{"not a list", `{"raw": "x"}`, "References must be a list of entries."},
{"bad doi", `[{"raw": "x", "doi": "not-a-doi"}]`, "Reference 1: DOI must look like 10.xxxx/suffix."},
{"bad orcid", `[{"raw": "x", "authors": [{"name": "A", "orcid": "0000-0002-1825-0098"}]}]`,
"Reference 1: ORCID must look like 0000-0002-1825-0097."},
{"bad url", `[{"raw": "x", "url": "ftp://example.com"}]`, "Reference 1: URL must be an http(s) address."},
{"bad arxiv", `[{"raw": "x", "arxiv": "not an id"}]`, "Reference 1: arXiv must be the bare identifier"},
{"no citation", `[{"doi": "10.1086/300499"}]`, "Reference 1 needs the citation itself"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := PostFromParams(map[string]string{
"title": "Hello", "slug": "hello", "body": "b", "refs": tc.refs,
}, nil)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("err = %v, want %q", err, tc.want)
}
})
}
}