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: → 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) { 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 } // 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) } } } deleted, err := c.ClearAllMetadata() if err != nil { t.Fatalf("ClearAllMetadata: %v", err) } if deleted != 2 { t.Errorf("deletedCount = %d, want 2 (files, not keys)", 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} { 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) } } } if got, err := c.GetByCID("bafkorphan"); err != nil || got != "" { t.Errorf("post-clear GetByCID(bafkorphan) = %q (err %v), want \"\"", got, 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 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"} { if _, _, ok, _ := c.GetKeyInfo(k); !ok { t.Errorf("clear deleted foreign key %q", k) } } if _, _, ok, _ := c.GetKeyInfo(CIDIndexPrefix + "bafkmine"); ok { t.Errorf("own reverse-index entry survived the clear") } } // 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)) }