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:
+41
-118
@@ -1,79 +1,27 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/metazla/meta-core/internal/kv"
|
||||
)
|
||||
|
||||
// newTestClient opens a fresh database in the test's temp dir.
|
||||
func newTestClient(t *testing.T) *Client {
|
||||
t.Helper()
|
||||
c := NewClient()
|
||||
if err := c.Connect(filepath.Join(t.TempDir(), "meta-core.db")); err != nil {
|
||||
t.Fatalf("connect: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = c.Close() })
|
||||
return c
|
||||
}
|
||||
|
||||
// allKeys lists every row (test helper; 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
|
||||
}
|
||||
|
||||
// TestClearAllMetadata_PurgesCIDReverseIndex is the regression test for the
|
||||
// leak: ClearAllMetadata used to delete every record key and the root index,
|
||||
// but left the cid:<cid> → root reverse-index entries behind. Those stale
|
||||
// aliases are not cosmetic — ResolveRoot consults them, so a surviving entry
|
||||
// silently redirects a later write into a deleted root.
|
||||
func TestClearAllMetadata_PurgesCIDReverseIndex(t *testing.T) {
|
||||
// ClearAllMetadata wipes BOTH databases: votes, signers and groups in truth;
|
||||
// merged records, the cid lookup, roots, search rows, the claim index and the
|
||||
// local files in the cache. Service scratch keys are not metadata and survive.
|
||||
func TestClearAllMetadataWipesTruthAndCache(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
|
||||
// Two records, each with several CID aliases (a midhash plus siblings) —
|
||||
// exactly what the watcher produces.
|
||||
aliases := map[string][]string{}
|
||||
for i, f := range []string{"watch/a.mkv", "watch/b.mkv"} {
|
||||
uuid, err := c.Mint(f, int64(100+i), int64(1000+i))
|
||||
if err != nil {
|
||||
t.Fatalf("mint: %v", err)
|
||||
}
|
||||
cids := []string{"bafkmid" + uuid, "bafkfull" + uuid}
|
||||
for _, cidStr := range cids {
|
||||
if err := c.AddAlias(uuid, cidStr); err != nil {
|
||||
t.Fatalf("add alias %s: %v", cidStr, err)
|
||||
}
|
||||
}
|
||||
if _, err := c.AddDuplicatePath(uuid, "watch/copy-"+f); err != nil {
|
||||
t.Fatalf("add duplicate: %v", err)
|
||||
}
|
||||
aliases[uuid] = cids
|
||||
a := signer(t, 1)
|
||||
mustAccept(t, c,
|
||||
vote(t, a, "bafkone", "title", "One", 1),
|
||||
vote(t, a, "bafkone", "cids/bafkonefull", "true", 1),
|
||||
vote(t, a, "bafktwo", "title", "Two", 1),
|
||||
)
|
||||
if _, _, err := c.RegisterFileTuple("/files/watch/a.mkv", 10, 20, "bafkmid"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// An entry already orphaned by a previous (buggy) clear: a reverse-index
|
||||
// key whose root no longer exists. The sweep is expected to collect it too.
|
||||
if err := c.SetRawValue(CIDIndexPrefix+"bafkorphan", "01OLDDEADROOT"); err != nil {
|
||||
t.Fatalf("seed orphan: %v", err)
|
||||
}
|
||||
|
||||
for uuid, cids := range aliases {
|
||||
for _, cidStr := range cids {
|
||||
if got := c.ResolveRoot(cidStr); got != uuid {
|
||||
t.Fatalf("pre-clear ResolveRoot(%s) = %q, want %q", cidStr, got, uuid)
|
||||
}
|
||||
for k, v := range map[string]string{"btmagnet:abc": "magnet:?xt=urn:btih:abc", "searchcov:tmdb:1": "1"} {
|
||||
if err := c.SetServiceValue(k, v); err != nil {
|
||||
t.Fatalf("seed %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,72 +29,47 @@ func TestClearAllMetadata_PurgesCIDReverseIndex(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("ClearAllMetadata: %v", err)
|
||||
}
|
||||
if deleted != 2 {
|
||||
t.Errorf("deletedCount = %d, want 2 (files, not keys)", deleted)
|
||||
if deleted != 3 {
|
||||
t.Errorf("deleted = %d, want 3 records", deleted)
|
||||
}
|
||||
|
||||
// Nothing record-shaped may survive: not the records, not their aliases,
|
||||
// duplicates or search rows, not the pre-existing orphan. (The event
|
||||
// streams and the schema sentinel are not metadata and stay.)
|
||||
for _, k := range allKeys(t, c) {
|
||||
for _, ns := range []string{recordNS, CIDIndexPrefix, rootsNS, ftsNS, dupNS} {
|
||||
for _, ns := range []string{recordNS, CIDIndexPrefix, rootsNS, ftsNS} {
|
||||
if strings.HasPrefix(k, ns) {
|
||||
t.Errorf("key survived the clear: %q", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
var ftsRows int
|
||||
if err := c.Store().DB().QueryRow(`SELECT count(*) FROM fts`).Scan(&ftsRows); err != nil || ftsRows != 0 {
|
||||
t.Errorf("fts rows after clear = %d (err %v), want 0", ftsRows, err)
|
||||
}
|
||||
|
||||
for _, cids := range aliases {
|
||||
for _, cidStr := range cids {
|
||||
if got := c.ResolveRoot(cidStr); got != cidStr {
|
||||
t.Errorf("post-clear ResolveRoot(%s) = %q, want the hash unchanged (stale alias)", cidStr, got)
|
||||
}
|
||||
for _, table := range []string{"fts", "claim_by_pid", "local_file"} {
|
||||
var n int
|
||||
if err := c.Store().DB().QueryRow(`SELECT count(*) FROM ` + table).Scan(&n); err != nil || n != 0 {
|
||||
t.Errorf("%s rows after clear = %d (err %v)", table, n, err)
|
||||
}
|
||||
}
|
||||
if got, err := c.GetByCID("bafkorphan"); err != nil || got != "" {
|
||||
t.Errorf("post-clear GetByCID(bafkorphan) = %q (err %v), want \"\"", got, err)
|
||||
if n, err := c.CountClaims(); err != nil || n != 0 {
|
||||
t.Errorf("claims after clear = %d (err %v)", n, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClearAllMetadata_LeavesForeignKeysAlone guards the sweep's blast radius:
|
||||
// keys outside the metadata namespaces (magnet cache, coverage markers, a key
|
||||
// that merely CONTAINS "cid:") must survive a metadata wipe.
|
||||
func TestClearAllMetadata_LeavesForeignKeysAlone(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
|
||||
uuid, err := c.Mint("watch/a.mkv", 100, 1000)
|
||||
if err != nil {
|
||||
t.Fatalf("mint: %v", err)
|
||||
if got := c.ResolveRoot("bafkone"); got != "bafkone" {
|
||||
t.Errorf("post-clear ResolveRoot = %q, want the hash unchanged", got)
|
||||
}
|
||||
if err := c.AddAlias(uuid, "bafkmine"); err != nil {
|
||||
t.Fatalf("add alias: %v", err)
|
||||
}
|
||||
for k, v := range map[string]string{"other:cid:bafktheirs": "01THEIRROOT", "btmagnet:abc": "magnet:?xt=urn:btih:abc", "unrelated": "keep-me"} {
|
||||
if err := c.SetRawValue(k, v); err != nil {
|
||||
t.Fatalf("seed %s: %v", k, err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := c.ClearAllMetadata(); err != nil {
|
||||
t.Fatalf("ClearAllMetadata: %v", err)
|
||||
}
|
||||
|
||||
for _, k := range []string{"other:cid:bafktheirs", "btmagnet:abc", "unrelated"} {
|
||||
for _, k := range []string{"btmagnet:abc", "searchcov:tmdb:1"} {
|
||||
if _, _, ok, _ := c.GetKeyInfo(k); !ok {
|
||||
t.Errorf("clear deleted foreign key %q", k)
|
||||
t.Errorf("clear deleted service key %q", k)
|
||||
}
|
||||
}
|
||||
if _, _, ok, _ := c.GetKeyInfo(CIDIndexPrefix + "bafkmine"); ok {
|
||||
t.Errorf("own reverse-index entry survived the clear")
|
||||
// The store keeps working after a wipe.
|
||||
mustAccept(t, c, vote(t, a, "bafkone", "title", "Again", 1))
|
||||
if got := record(t, c, "bafkone")["title"]; got != "Again" {
|
||||
t.Errorf("title after re-write = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// withClient runs fn against a fresh client.
|
||||
func withClient(t *testing.T, fn func(t *testing.T, c *Client)) {
|
||||
t.Helper()
|
||||
fn(t, newTestClient(t))
|
||||
// Raw writes are limited to service scratch keys: merged records and
|
||||
// bookkeeping cannot be written around the claims.
|
||||
func TestServiceValueRefusesRecordKeys(t *testing.T) {
|
||||
c := newTestClient(t)
|
||||
for _, k := range []string{"file:R1/title", "cid:bafk", "idx:roots/R1", "ev:meta:events/1", "meta:applied_seq"} {
|
||||
if err := c.SetServiceValue(k, "x"); err == nil {
|
||||
t.Errorf("SetServiceValue(%q) accepted", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user