// Copyright (c) 2026 Petr Balvín (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" ) // The write form normalises identifiers as it stores them: a doi.org // URL becomes the bare DOI, an iD gains its upper-case X. func TestIdentifiersNormaliseOnWrite(t *testing.T) { p, err := PostFromParams(map[string]string{ "title": "A paper", "slug": "a-paper", "body": "b", "doi": "https://doi.org/10.5281/zenodo.1234567", "orcid": "0000-0000-0000-001x", }, nil) if err != nil { t.Fatalf("PostFromParams: %v", err) } if got := p.DOI(); got != "10.5281/zenodo.1234567" { t.Fatalf("stored doi = %q", got) } if got := p.ORCID(); got != "0000-0000-0000-001X" { t.Fatalf("stored orcid = %q", got) } // Identifiers are reserved: they never appear in the custom fields. if fields := p.CustomFields(); fields != nil { t.Fatalf("CustomFields leaked identifiers: %v", fields) } s := newStoreWithPosts(t, nil) if err := CreationError(p, s, nil); err != nil { t.Fatalf("valid identifiers refused: %v", err) } } func TestIdentifierValidationErrors(t *testing.T) { s := newStoreWithPosts(t, nil) base := func(key, value string) *post.Post { meta := frontmatter.NewMeta() meta.Set("slug", "x") meta.Set("title", "T") meta.Set(key, value) return post.New(meta, "body") } cases := []struct { key, value, want string }{ {"doi", "20.1234/x", "DOI must look like 10.xxxx/suffix."}, {"doi", "10.1", "DOI must look like 10.xxxx/suffix."}, {"orcid", "0000-0002-1825-0098", "ORCID must look like 0000-0002-1825-0097."}, {"orcid", "not-an-orcid", "ORCID must look like 0000-0002-1825-0097."}, } for _, tc := range cases { err := CreationError(base(tc.key, tc.value), s, nil) if err == nil || !strings.Contains(err.Error(), tc.want) { t.Errorf("%s %q: error = %v, want %q", tc.key, tc.value, err, tc.want) } } // Both empty stay empty: the fields remain optional. if err := CreationError(base("doi", ""), s, nil); err != nil { t.Errorf("empty doi refused: %v", err) } } // The read shapes carry the identifiers, and the SEO block publishes // them: an identifier property on the article, one on its author. func TestIdentifiersInPayloads(t *testing.T) { meta := frontmatter.NewMeta() meta.Set("slug", "paper") meta.Set("title", "Paper") meta.Set("author", "Petr Balvín") meta.Set("doi", "https://doi.org/10.1000/xyz") meta.Set("orcid", "0000-0002-1825-0097") p := post.New(meta, "body text here") summary := BuildSummary(p) if summary.DOI != "10.1000/xyz" { t.Fatalf("summary doi = %q", summary.DOI) } if summary.ORCID != "0000-0002-1825-0097" { t.Fatalf("summary orcid = %q", summary.ORCID) } m, err := BuildMeta(p, "https://example.com") if err != nil { t.Fatalf("BuildMeta: %v", err) } for _, want := range []string{ `"identifier":"https://doi.org/10.1000/xyz"`, `"identifier":"https://orcid.org/0000-0002-1825-0097"`, } { if !strings.Contains(m.JSONLD, want) { t.Fatalf("json_ld missing %s:\n%s", want, m.JSONLD) } } // A stored value that is no identifier at all survives the read // untouched rather than vanishing. broken := post.New(func() *frontmatter.Meta { m := frontmatter.NewMeta() m.Set("slug", "broken") m.Set("doi", "10.ONE_HUNDRED/x") return m }(), "b") if got := BuildSummary(broken).DOI; got != "10.ONE_HUNDRED/x" { t.Fatalf("invalid stored doi dropped on read: %q", got) } }