Files

167 lines
4.1 KiB
Go

package storage
import (
"context"
"fmt"
"path/filepath"
"testing"
"github.com/metazla/meta-core/internal/claim"
"github.com/metazla/meta-core/internal/identity"
"github.com/metazla/meta-core/internal/kv"
)
// openClient opens (and starts) a client on dir's truth + cache files.
func openClient(t *testing.T, dir string) *Client {
t.Helper()
c := NewClient()
if err := c.Connect(filepath.Join(dir, "meta-core.db"), filepath.Join(dir, "cache.db")); err != nil {
t.Fatalf("connect: %v", err)
}
if err := c.EnsureSchemaVersion(SchemaVersion); err != nil {
t.Fatalf("schema: %v", err)
}
if err := c.Start(); err != nil {
t.Fatalf("start: %v", err)
}
return c
}
// newTestClient opens a fresh store in the test's temp dir.
func newTestClient(t *testing.T) *Client {
t.Helper()
c := openClient(t, t.TempDir())
t.Cleanup(func() { _ = c.Close() })
return c
}
// signer returns a deterministic test identity (scalar n repeated).
func signer(t *testing.T, n byte) *identity.Identity {
t.Helper()
id, err := identity.Import(fmt.Sprintf("%02x%062x", n, 1))
if err != nil {
t.Fatalf("identity: %v", err)
}
return id
}
// vote builds a signed claim; v is a string (text), []byte (ciphertext) or nil
// (retraction).
func vote(t *testing.T, id *identity.Identity, cid, key string, v any, ts uint64) claim.Claim {
t.Helper()
cl := claim.Claim{CID: cid, UID: id.UID, Key: key, TS: ts}
switch x := v.(type) {
case string:
cl.Value = claim.Text(x)
case []byte:
cl.Value = claim.Bytes(x)
case nil:
default:
t.Fatalf("vote value %T", v)
}
if err := cl.Sign(id); err != nil {
t.Fatalf("sign: %v", err)
}
return cl
}
// write posts claims and returns their statuses.
func write(t *testing.T, c *Client, claims ...claim.Claim) []ClaimStatus {
t.Helper()
res, err := c.WriteClaims(context.Background(), claims)
if err != nil {
t.Fatalf("write: %v", err)
}
out := make([]ClaimStatus, len(res))
for i, r := range res {
out[i] = r.Status
}
return out
}
// mustAccept posts claims and fails unless every one is accepted.
func mustAccept(t *testing.T, c *Client, claims ...claim.Claim) {
t.Helper()
for i, st := range write(t, c, claims...) {
if st != StatusAccepted {
t.Fatalf("claim %d (%s/%s): %s", i, claims[i].CID, claims[i].Key, st)
}
}
}
// seed writes merged rows straight into the cache — the derived layout search
// and the counters work on — bypassing claims. Search tests pin SQL behaviour
// over that layout; how rows get there is the apply loop's business.
func seed(t *testing.T, c *Client, records map[string]map[string]string) {
t.Helper()
for id, fields := range records {
setMerged(t, c, id, fields)
}
}
func setMerged(t *testing.T, c *Client, rid string, fields map[string]string) {
t.Helper()
if err := c.update(func(tx *kv.Tx) error {
for k, v := range fields {
if err := tx.Put(recordPrefix(rid)+k, v); err != nil {
return err
}
}
if err := tx.Put(rootKey(rid), ""); err != nil {
return err
}
return refreshSearchTx(tx, rid)
}); err != nil {
t.Fatalf("seed %s: %v", rid, err)
}
}
func deleteMerged(t *testing.T, c *Client, rid string) {
t.Helper()
if err := c.update(func(tx *kv.Tx) error {
if _, err := tx.DeletePrefix(recordPrefix(rid), 0); err != nil {
return err
}
if _, err := tx.Delete(rootKey(rid)); err != nil {
return err
}
return deleteSearchRowTx(tx, rid)
}); err != nil {
t.Fatalf("delete %s: %v", rid, err)
}
}
// allKeys lists every cache kv row (full scan).
func allKeys(t *testing.T, c *Client) []string {
t.Helper()
var keys []string
if err := c.Store().ScanPrefix(context.Background(), "", 1000, func(rows []kv.KV) error {
for _, r := range rows {
keys = append(keys, r.K)
}
return nil
}); err != nil {
t.Fatalf("scan: %v", err)
}
return keys
}
// rid resolves a cid to its record (fails when unknown).
func rid(t *testing.T, c *Client, cid string) string {
t.Helper()
r, err := c.GetByCID(cid)
if err != nil || r == "" {
t.Fatalf("cid %s unresolved (%v)", cid, err)
}
return r
}
func record(t *testing.T, c *Client, cid string) map[string]string {
t.Helper()
f, err := c.GetMetadataFlat(rid(t, c, cid))
if err != nil {
t.Fatal(err)
}
return f
}