126 lines
3.9 KiB
Go
126 lines
3.9 KiB
Go
package attesto
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func receipt(seqNo int64, eventHash, prevEventHash string) EventReceipt {
|
|
return EventReceipt{
|
|
StreamID: "str_demo",
|
|
SeqNo: seqNo,
|
|
EventHash: eventHash,
|
|
PrevEventHash: prevEventHash,
|
|
}
|
|
}
|
|
|
|
func TestMemoryHeadStoreInOrderAdvances(t *testing.T) {
|
|
store := NewMemoryHeadStore()
|
|
for _, r := range []EventReceipt{receipt(1, "h1", ""), receipt(2, "h2", "h1"), receipt(3, "h3", "h2")} {
|
|
if err := checkAndAdvanceHead(store, r); err != nil {
|
|
t.Fatalf("unexpected: %v", err)
|
|
}
|
|
}
|
|
if seq, hash, ok := store.Get("str_demo"); !ok || seq != 3 || hash != "h3" {
|
|
t.Errorf("head = (%d,%s,%v)", seq, hash, ok)
|
|
}
|
|
}
|
|
|
|
func TestForgedRewoundReceiptIsFork(t *testing.T) {
|
|
store := NewMemoryHeadStore()
|
|
_ = checkAndAdvanceHead(store, receipt(1, "h1", ""))
|
|
_ = checkAndAdvanceHead(store, receipt(2, "h2", "h1"))
|
|
err := checkAndAdvanceHead(store, receipt(2, "h2-fork", "h1"))
|
|
var fork *ForkDetectedError
|
|
if !errors.As(err, &fork) {
|
|
t.Fatalf("expected *ForkDetectedError, got %v", err)
|
|
}
|
|
if seq, hash, _ := store.Get("str_demo"); seq != 2 || hash != "h2" {
|
|
t.Errorf("store advanced past fork: (%d,%s)", seq, hash)
|
|
}
|
|
}
|
|
|
|
func TestDivergentNextEventIsFork(t *testing.T) {
|
|
store := NewMemoryHeadStore()
|
|
_ = checkAndAdvanceHead(store, receipt(1, "h1", ""))
|
|
err := checkAndAdvanceHead(store, receipt(2, "h2", "WRONG"))
|
|
var fork *ForkDetectedError
|
|
if !errors.As(err, &fork) {
|
|
t.Fatalf("expected *ForkDetectedError, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestForwardGapAccepted(t *testing.T) {
|
|
store := NewMemoryHeadStore()
|
|
_ = checkAndAdvanceHead(store, receipt(1, "h1", ""))
|
|
if err := checkAndAdvanceHead(store, receipt(5, "h5", "h4")); err != nil {
|
|
t.Fatalf("forward gap should be accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestFileHeadStorePersistsAndIs0600(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "heads.json")
|
|
store := NewFileHeadStore(path)
|
|
_ = checkAndAdvanceHead(store, receipt(1, "h1", ""))
|
|
_ = checkAndAdvanceHead(store, receipt(2, "h2", "h1"))
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if info.Mode().Perm() != 0o600 {
|
|
t.Errorf("mode = %o, want 600", info.Mode().Perm())
|
|
}
|
|
reopened := NewFileHeadStore(path)
|
|
if seq, hash, ok := reopened.Get("str_demo"); !ok || seq != 2 || hash != "h2" {
|
|
t.Errorf("reopened head = (%d,%s,%v)", seq, hash, ok)
|
|
}
|
|
if err := checkAndAdvanceHead(reopened, receipt(2, "h2-fork", "h1")); err == nil {
|
|
t.Error("expected fork on reopened store")
|
|
}
|
|
}
|
|
|
|
func TestFileHeadStorePersistenceFailureReturned(t *testing.T) {
|
|
path := t.TempDir()
|
|
store := NewFileHeadStore(path)
|
|
if err := checkAndAdvanceHead(store, receipt(1, "h1", "")); err == nil {
|
|
t.Fatal("expected persistence failure when head-store path is a directory")
|
|
}
|
|
}
|
|
|
|
func TestFileHeadStoreCorruptFileFailsClosed(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "heads.json")
|
|
if err := os.WriteFile(path, []byte("{not-json"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
store := NewFileHeadStore(path)
|
|
if err := store.SetWithError("str_demo", 1, "h1"); err == nil {
|
|
t.Fatal("expected corrupt head store to fail closed")
|
|
}
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(raw) != "{not-json" {
|
|
t.Fatal("corrupt head store must not be silently overwritten")
|
|
}
|
|
}
|
|
|
|
func TestExactReplayOfStoredHeadIsBenign(t *testing.T) {
|
|
// [P3.3 regression] A deduplicated resend returns the same receipt; the
|
|
// head tracker must treat (same seqNo, same eventHash) as a no-op.
|
|
store := NewMemoryHeadStore()
|
|
first := EventReceipt{StreamID: "str_x", SeqNo: 1, EventHash: "h1"}
|
|
if err := checkAndAdvanceHead(store, first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := checkAndAdvanceHead(store, first); err != nil {
|
|
t.Fatalf("exact replay must be benign, got %v", err)
|
|
}
|
|
fork := EventReceipt{StreamID: "str_x", SeqNo: 1, EventHash: "h2"}
|
|
if err := checkAndAdvanceHead(store, fork); err == nil {
|
|
t.Fatal("same seq with different hash must be a fork")
|
|
}
|
|
}
|