package blobstore_test import ( "bytes" "crypto/sha256" "encoding/hex" "net" "testing" "time" "github.com/enmanuel/unibus/pkg/blobstore" "github.com/enmanuel/unibus/pkg/embeddednats" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" ) func objFreePort(t *testing.T) int { t.Helper() l, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatalf("free port: %v", err) } defer l.Close() return l.Addr().(*net.TCPAddr).Port } // newObjectStore boots a single-node embedded NATS with JetStream and returns a // replicated (R1) Object Store backend over it. func newObjectStore(t *testing.T) blobstore.Store { t.Helper() ns, err := embeddednats.StartServer(embeddednats.ServerConfig{ StoreDir: t.TempDir(), Host: "127.0.0.1", Port: objFreePort(t), }) if err != nil { t.Fatalf("embedded nats: %v", err) } nc, err := nats.Connect(ns.ClientURL()) if err != nil { ns.Shutdown() t.Fatalf("nats connect: %v", err) } js, err := jetstream.New(nc) if err != nil { nc.Close() ns.Shutdown() t.Fatalf("jetstream: %v", err) } st, err := blobstore.NewObjectStore(js, blobstore.ObjectStoreConfig{Replicas: 1, OpTimeout: 5 * time.Second}) if err != nil { nc.Close() ns.Shutdown() t.Fatalf("new object store: %v", err) } t.Cleanup(func() { nc.Close(); ns.Shutdown(); ns.WaitForShutdown() }) return st } // TestObjectStoreRoundTrip is the golden path: put ciphertext, get it back by // its hash, Has reports presence, and re-putting identical bytes returns the // same address (content-addressed dedup). func TestObjectStoreRoundTrip(t *testing.T) { s := newObjectStore(t) data := []byte("encrypted-media-ciphertext-payload") hash, err := s.Put(data) if err != nil { t.Fatalf("Put: %v", err) } want := hex.EncodeToString(sha256Sum(data)) if hash != want { t.Fatalf("hash = %q, want sha256 hex %q", hash, want) } got, err := s.Get(hash) if err != nil { t.Fatalf("Get: %v", err) } if !bytes.Equal(got, data) { t.Fatalf("Get returned %q, want %q", got, data) } if !s.Has(hash) { t.Fatalf("Has should be true for a stored blob") } // Re-put identical bytes: same address, no error. hash2, err := s.Put(data) if err != nil || hash2 != hash { t.Fatalf("re-Put: hash2=%q err=%v (want %q)", hash2, err, hash) } } // TestObjectStoreMissing is the edge/error path: a hash that was never stored // is absent and unreadable. func TestObjectStoreMissing(t *testing.T) { s := newObjectStore(t) missing := hex.EncodeToString(sha256Sum([]byte("never stored"))) if s.Has(missing) { t.Fatalf("Has should be false for an unknown hash") } if _, err := s.Get(missing); err == nil { t.Fatalf("Get of an unknown hash should error") } } // TestObjectStoreAddressMatchesDisk is the contract test: the Object Store and // the disk backend address identical bytes to the IDENTICAL hash, so a client // cannot tell which backend a node uses and a blob ref is portable across them. func TestObjectStoreAddressMatchesDisk(t *testing.T) { obj := newObjectStore(t) disk, err := blobstore.New(t.TempDir()) if err != nil { t.Fatalf("disk store: %v", err) } for _, payload := range [][]byte{[]byte("a"), []byte("longer ciphertext blob \x00\x01\x02"), {}} { oh, err := obj.Put(payload) if err != nil { t.Fatalf("object Put: %v", err) } dh, err := disk.Put(payload) if err != nil { t.Fatalf("disk Put: %v", err) } if oh != dh { t.Fatalf("address mismatch for %q: object=%q disk=%q", payload, oh, dh) } } } func sha256Sum(b []byte) []byte { sum := sha256.Sum256(b) return sum[:] }