Add receipt_format to stamp and verify. Omitted → text; receipt|PDF-Receipt → PDF; JSON-Receipt → JSON; MD|MD-Receipt; CSV|CSV-Receipt; TSV|TSV-Receipt. Stamp always returns receipts[] and adds artifacts[] in the chosen format. Stdio writes ~/.verae/receipts (0600); public MCP returns content_text/content_base64 only. Verify accepts inline receipt or receipt_path (stdio), parses id, and re-emits an artifact. Vendors pdfcert (+ slim statuslang, gofpdf) for CERT-01 PDF. Also lands welcome tool and email-username helpers already wired into register/public paths.
187 lines
4.8 KiB
Go
187 lines
4.8 KiB
Go
package tools
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormalizeReceiptFormat(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
want string
|
|
err bool
|
|
}{
|
|
{"", KindText, false},
|
|
{" ", KindText, false},
|
|
{"text", KindText, false},
|
|
{"receipt", KindPDF, false},
|
|
{"PDF-Receipt", KindPDF, false},
|
|
{"pdf-receipt", KindPDF, false},
|
|
{"PDF", KindPDF, false},
|
|
{"JSON-Receipt", KindJSON, false},
|
|
{"json", KindJSON, false},
|
|
{"MD", KindMD, false},
|
|
{"MD-Receipt", KindMD, false},
|
|
{"markdown", KindMD, false},
|
|
{"CSV", KindCSV, false},
|
|
{"CSV-Receipt", KindCSV, false},
|
|
{"csv", KindCSV, false},
|
|
{"TSV", KindTSV, false},
|
|
{"TSV-Receipt", KindTSV, false},
|
|
{"tsv", KindTSV, false},
|
|
{"docx", "", true},
|
|
{"xml", "", true},
|
|
}
|
|
for _, tc := range cases {
|
|
got, err := normalizeReceiptFormat(tc.in)
|
|
if tc.err {
|
|
if err == nil {
|
|
t.Fatalf("normalizeReceiptFormat(%q) expected error", tc.in)
|
|
}
|
|
continue
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("normalizeReceiptFormat(%q): %v", tc.in, err)
|
|
}
|
|
if got != tc.want {
|
|
t.Fatalf("normalizeReceiptFormat(%q)=%q want %q", tc.in, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sampleDTO() ReceiptDTO {
|
|
return ReceiptDTO{
|
|
ReceiptID: "QmWRJoKLTs56xFQLkhtvSQnWiQvebXGRqRdtbsjodm5HVX",
|
|
Hash: "01ec26e0bcdaf0271af7bb7810b1eae22b3220bc3c59b9cc7abfa1519edcbae1",
|
|
Scope: "global",
|
|
CreatedAt: "2026-09-23T14:46:08Z",
|
|
Merkle: "merkle-root-example",
|
|
}
|
|
}
|
|
|
|
func TestBuildArtifactFormats(t *testing.T) {
|
|
dto := sampleDTO()
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
|
|
for _, kind := range []string{KindText, KindJSON, KindMD, KindCSV, KindTSV, KindPDF} {
|
|
art, err := buildArtifact(dto, kind, true)
|
|
if err != nil {
|
|
t.Fatalf("buildArtifact(%s): %v", kind, err)
|
|
}
|
|
if art.Format != kind {
|
|
t.Fatalf("format=%q want %q", art.Format, kind)
|
|
}
|
|
if art.MediaType != mediaTypeForKind(kind) {
|
|
t.Fatalf("%s media_type=%q", kind, art.MediaType)
|
|
}
|
|
if art.Path == "" {
|
|
t.Fatalf("%s: expected path under HOME", kind)
|
|
}
|
|
if _, err := os.Stat(art.Path); err != nil {
|
|
t.Fatalf("%s path missing: %v", kind, err)
|
|
}
|
|
switch kind {
|
|
case KindPDF:
|
|
if art.ContentB64 == "" {
|
|
t.Fatal("pdf: empty content_base64")
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(art.ContentB64)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.HasPrefix(string(b), "%PDF") {
|
|
t.Fatalf("pdf does not start with %%PDF")
|
|
}
|
|
case KindJSON:
|
|
if !strings.Contains(art.ContentText, `"format": "JSON-Receipt"`) {
|
|
t.Fatalf("json missing format marker: %s", art.ContentText[:min(80, len(art.ContentText))])
|
|
}
|
|
if !strings.Contains(art.ContentText, dto.ReceiptID) {
|
|
t.Fatal("json missing receipt_id")
|
|
}
|
|
case KindMD:
|
|
if !strings.Contains(art.ContentText, "| receipt_id |") {
|
|
t.Fatalf("md missing table header: %s", art.ContentText)
|
|
}
|
|
case KindCSV:
|
|
if !strings.HasPrefix(art.ContentText, "receipt_id,") {
|
|
t.Fatalf("csv header: %q", art.ContentText)
|
|
}
|
|
if !strings.Contains(art.ContentText, dto.ReceiptID) {
|
|
t.Fatal("csv missing id")
|
|
}
|
|
case KindTSV:
|
|
if !strings.HasPrefix(art.ContentText, "receipt_id\t") {
|
|
t.Fatalf("tsv header: %q", art.ContentText)
|
|
}
|
|
case KindText:
|
|
if !strings.Contains(art.ContentText, "receipt_id: "+dto.ReceiptID) {
|
|
t.Fatal("text missing receipt_id line")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildArtifactPublicNoDisk(t *testing.T) {
|
|
home := t.TempDir()
|
|
t.Setenv("HOME", home)
|
|
art, err := buildArtifact(sampleDTO(), KindJSON, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if art.Path != "" {
|
|
t.Fatalf("public must not set path, got %q", art.Path)
|
|
}
|
|
ents, _ := os.ReadDir(filepath.Join(home, ".verae", "receipts"))
|
|
if len(ents) != 0 {
|
|
t.Fatalf("public wrote %d files under receipts", len(ents))
|
|
}
|
|
if art.ContentText == "" {
|
|
t.Fatal("expected content_text")
|
|
}
|
|
}
|
|
|
|
func TestParseReceiptIDRoundTrip(t *testing.T) {
|
|
dto := sampleDTO()
|
|
cases := []string{KindText, KindJSON, KindMD, KindCSV, KindTSV}
|
|
for _, kind := range cases {
|
|
art, err := buildArtifact(dto, kind, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := parseReceiptIDFromBody(kind, art.ContentText)
|
|
if err != nil {
|
|
t.Fatalf("parse %s: %v\nbody:\n%s", kind, err, art.ContentText)
|
|
}
|
|
if id != dto.ReceiptID {
|
|
t.Fatalf("parse %s: got %q want %q", kind, id, dto.ReceiptID)
|
|
}
|
|
}
|
|
// PDF via base64
|
|
art, err := buildArtifact(dto, KindPDF, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
id, err := parseReceiptIDFromBody(KindPDF, art.ContentB64)
|
|
if err != nil {
|
|
t.Fatalf("parse pdf: %v", err)
|
|
}
|
|
if id != dto.ReceiptID {
|
|
t.Fatalf("parse pdf: got %q want %q", id, dto.ReceiptID)
|
|
}
|
|
}
|
|
|
|
func TestAttachArtifacts(t *testing.T) {
|
|
out := ReceiptsOut{Receipts: []ReceiptDTO{sampleDTO()}}
|
|
got, err := attachArtifacts(out, KindCSV, false)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Artifacts) != 1 || got.Artifacts[0].Format != KindCSV {
|
|
t.Fatalf("%+v", got.Artifacts)
|
|
}
|
|
}
|