package idempotency import ( "os" "path/filepath" "sync" "testing" ) func TestPutGet(t *testing.T) { s, err := Open(filepath.Join(t.TempDir(), "idem.log")) if err != nil { t.Fatal(err) } defer s.Close() if _, ok := s.Get("a"); ok { t.Fatal("unexpected hit on empty store") } if err := s.Put("a", 42); err != nil { t.Fatal(err) } if id, ok := s.Get("a"); !ok || id != 42 { t.Fatalf("Get(a) = %d,%v; want 42,true", id, ok) } } func TestFirstWriterWins(t *testing.T) { s, _ := Open(filepath.Join(t.TempDir(), "idem.log")) defer s.Close() _ = s.Put("a", 1) _ = s.Put("a", 2) // ignored if id, _ := s.Get("a"); id != 1 { t.Fatalf("got %d, want first value 1", id) } } func TestDurableAcrossReopen(t *testing.T) { path := filepath.Join(t.TempDir(), "idem.log") s, _ := Open(path) _ = s.Put("checkout-x", 1001) _ = s.Put("checkout-y", 1002) _ = s.Close() // A fresh process (the restart case the in-memory map failed) must still see // the recorded keys — this is the duplicate-order bug the durable store fixes. s2, err := Open(path) if err != nil { t.Fatal(err) } defer s2.Close() if id, ok := s2.Get("checkout-x"); !ok || id != 1001 { t.Fatalf("after reopen Get(checkout-x) = %d,%v; want 1001,true", id, ok) } if id, ok := s2.Get("checkout-y"); !ok || id != 1002 { t.Fatalf("after reopen Get(checkout-y) = %d,%v; want 1002,true", id, ok) } } func TestCorruptTrailingLineSkipped(t *testing.T) { path := filepath.Join(t.TempDir(), "idem.log") s, _ := Open(path) _ = s.Put("good", 7) _ = s.Close() // Simulate a torn write from a crash mid-append. f, _ := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0o644) _, _ = f.WriteString(`{"k":"bad","id":`) _ = f.Close() s2, err := Open(path) if err != nil { t.Fatalf("open with corrupt tail: %v", err) } defer s2.Close() if id, ok := s2.Get("good"); !ok || id != 7 { t.Fatalf("good key lost after corrupt tail: %d,%v", id, ok) } if _, ok := s2.Get("bad"); ok { t.Fatal("corrupt line should not have loaded") } } func TestLockSerialisesKey(t *testing.T) { s, _ := Open(filepath.Join(t.TempDir(), "idem.log")) defer s.Close() // Two goroutines contend on the same key; the lock must serialise the // check→create→record so exactly one writes and both agree on the value. var wg sync.WaitGroup for i := 0; i < 2; i++ { wg.Add(1) go func(v uint64) { defer wg.Done() unlock := s.Lock("k") defer unlock() if _, ok := s.Get("k"); !ok { _ = s.Put("k", v) } }(uint64(i + 1)) } wg.Wait() if _, ok := s.Get("k"); !ok { t.Fatal("expected a value recorded under contention") } }