232 lines
7.3 KiB
Go
232 lines
7.3 KiB
Go
// 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)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|