verae-mcp-timestamping-serv.../internal/ledger/file_test.go
George Lambert 793746fcd0 feat: FileLedger for local and shared scopes
JSON file ledger with Stamp/Verify/HashReport, idempotent hash+org+scope,
mutex safety, mode 0600 / parent 0700.
2026-09-29 12:01:57 -04:00

251 lines
6.2 KiB
Go

package ledger_test
import (
"os"
"path/filepath"
"strings"
"sync"
"testing"
"git.georgelambert.org/marchon/verae-mcp-timestamping-server-go/internal/ledger"
)
const sha256Empty = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
func TestStamp_ReturnsOneReceipt(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
recs, err := l.Stamp(sha256Empty, "local", "", "")
if err != nil {
t.Fatal(err)
}
if len(recs) != 1 {
t.Fatalf("len=%d", len(recs))
}
rec := recs[0]
if rec.Hash != sha256Empty || rec.Scope != "local" {
t.Fatalf("rec=%+v", rec)
}
if rec.ReceiptID == "" || rec.CreatedAt == "" || rec.Merkle == "" {
t.Fatalf("missing fields: %+v", rec)
}
}
func TestStamp_IdempotentSameHashOrgScope(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
first, err := l.Stamp(sha256Empty, "local", "cube-a", "org-1")
if err != nil {
t.Fatal(err)
}
second, err := l.Stamp(sha256Empty, "local", "cube-b", "org-1")
if err != nil {
t.Fatal(err)
}
if first[0].ReceiptID != second[0].ReceiptID || first[0].CreatedAt != second[0].CreatedAt {
t.Fatalf("not idempotent: %+v vs %+v", first[0], second[0])
}
}
func TestStamp_DifferentOrgOrScopeIsNew(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
a, err := l.Stamp(sha256Empty, "local", "", "org-1")
if err != nil {
t.Fatal(err)
}
b, err := l.Stamp(sha256Empty, "local", "", "org-2")
if err != nil {
t.Fatal(err)
}
c, err := l.Stamp(sha256Empty, "shared", "", "org-1")
if err != nil {
t.Fatal(err)
}
if a[0].ReceiptID == b[0].ReceiptID || a[0].ReceiptID == c[0].ReceiptID {
t.Fatalf("expected distinct receipts")
}
}
func TestStamp_NormalizesPrefix(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
recs, err := l.Stamp("sha256:"+strings.ToUpper(sha256Empty), "local", "", "")
if err != nil {
t.Fatal(err)
}
if recs[0].Hash != sha256Empty {
t.Fatalf("hash=%q", recs[0].Hash)
}
}
func TestStamp_RejectsBadDigestAndScope(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
if _, err := l.Stamp("not-a-hash", "local", "", ""); err == nil {
t.Fatal("expected digest error")
}
if _, err := l.Stamp(sha256Empty, "nope", "", ""); err == nil {
t.Fatal("expected scope error")
}
}
func TestVerify_AndScopeRank(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
recs, err := l.Stamp(sha256Empty, "local", "", "")
if err != nil {
t.Fatal(err)
}
ok := l.Verify(recs[0].ReceiptID, "local")
if !ok.Valid || ok.Receipt == nil || ok.Receipt.ReceiptID != recs[0].ReceiptID {
t.Fatalf("ok=%+v", ok)
}
weak := l.Verify(recs[0].ReceiptID, "global")
if weak.Valid {
t.Fatalf("local cannot satisfy global: %+v", weak)
}
missing := l.Verify("no-such", "")
if missing.Valid || missing.Error == "" {
t.Fatalf("missing=%+v", missing)
}
}
func TestHashReport_Filters(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
other := strings.Repeat("ab", 32)
if _, err := l.Stamp(sha256Empty, "local", "", "org-1"); err != nil {
t.Fatal(err)
}
if _, err := l.Stamp(sha256Empty, "shared", "", "org-1"); err != nil {
t.Fatal(err)
}
if _, err := l.Stamp(sha256Empty, "local", "", "org-2"); err != nil {
t.Fatal(err)
}
if _, err := l.Stamp(other, "local", "", "org-1"); err != nil {
t.Fatal(err)
}
all, err := l.HashReport(sha256Empty, "", "")
if err != nil || len(all) != 3 {
t.Fatalf("all=%d err=%v", len(all), err)
}
org1, err := l.HashReport(sha256Empty, "org-1", "")
if err != nil || len(org1) != 2 {
t.Fatalf("org1=%d err=%v", len(org1), err)
}
shared, err := l.HashReport(sha256Empty, "", "shared")
if err != nil || len(shared) != 1 || shared[0].Scope != "shared" {
t.Fatalf("shared=%+v err=%v", shared, err)
}
}
func TestStamp_SurvivesReload(t *testing.T) {
path := filepath.Join(t.TempDir(), "subdir", "receipts.json")
first, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
recs, err := first.Stamp(sha256Empty, "local", "", "")
if err != nil {
t.Fatal(err)
}
second, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
report, err := second.HashReport(sha256Empty, "", "")
if err != nil || len(report) != 1 || report[0].ReceiptID != recs[0].ReceiptID {
t.Fatalf("report=%+v err=%v", report, err)
}
ok := second.Verify(recs[0].ReceiptID, "local")
if !ok.Valid {
t.Fatalf("verify after reload: %+v", ok)
}
}
func TestSave_FileMode0600Parent0700(t *testing.T) {
dir := filepath.Join(t.TempDir(), "nested")
path := filepath.Join(dir, "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
if _, err := l.Stamp(sha256Empty, "local", "", ""); err != nil {
t.Fatal(err)
}
fi, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
if fi.Mode().Perm() != 0o600 {
t.Fatalf("file mode=%o want 0600", fi.Mode().Perm())
}
di, err := os.Stat(dir)
if err != nil {
t.Fatal(err)
}
if di.Mode().Perm() != 0o700 {
t.Fatalf("dir mode=%o want 0700", di.Mode().Perm())
}
}
func TestStamp_ConcurrentIdempotent(t *testing.T) {
path := filepath.Join(t.TempDir(), "receipts.json")
l, err := ledger.Open(path)
if err != nil {
t.Fatal(err)
}
var wg sync.WaitGroup
ids := make([]string, 20)
errs := make([]error, 20)
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
recs, err := l.Stamp(sha256Empty, "local", "", "org")
errs[i] = err
if err == nil && len(recs) == 1 {
ids[i] = recs[0].ReceiptID
}
}(i)
}
wg.Wait()
for i, err := range errs {
if err != nil {
t.Fatalf("i=%d: %v", i, err)
}
}
for i := 1; i < len(ids); i++ {
if ids[i] != ids[0] {
t.Fatalf("ids diverge: %q vs %q", ids[0], ids[i])
}
}
report, err := l.HashReport(sha256Empty, "org", "")
if err != nil || len(report) != 1 {
t.Fatalf("want 1 receipt after concurrent stamps, got %d err=%v", len(report), err)
}
}