v2.0.0: claims model - signed MMUR2 votes in a truth DB, merged view in a rebuildable cache DB; POST /api/claims, claims and merge routes, unsigned writes answer 410
Build and Push Docker Image / build (push) Successful in 5m1s
Build and Push Docker Image / build (push) Successful in 5m1s
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user