JSON file ledger with Stamp/Verify/HashReport, idempotent hash+org+scope, mutex safety, mode 0600 / parent 0700.
251 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|