package mute import ( "encoding/hex" "os" "testing" "git.smesh.lol/smesh/pkg/nostr/event" "git.smesh.lol/smesh/pkg/nostr/filter" "git.smesh.lol/smesh/pkg/nostr/kind" "git.smesh.lol/smesh/pkg/nostr/tag" "git.smesh.lol/smesh/pkg/store" ) // mutePk is a fixed 32-byte pubkey so the store round-trip needs no signer. func mutePk(fill byte) (b []byte) { b = []byte{:32} for i := range b { b[i] = fill } return } func muteSig() (b []byte) { b = []byte{:64} for i := range b { b[i] = byte(i) } return } func muteTmp(t *testing.T) (eng *store.Engine, dir string) { t.Helper() dir, derr := os.MkdirTemp("", "moxie-mute") if derr != nil { t.Fatal(derr) } eng, oerr := store.Open(dir) if oerr != nil { t.Fatal(oerr) } return eng, dir } // muteKind materializes the kind table before reading the package pointer: // kind.MuteList is nil until kind.Ensure() runs. func muteKind() (k uint16) { kind.Ensure() return kind.MuteList.K } // muteEvent builds an event with one p-tag per raw value. The caller chooses // the value form: 32 raw bytes, 64 hex chars, or the 33-byte binary form. func muteEvent(t *testing.T, pub []byte, idFill byte, k uint16, vals [][]byte) (ev *event.E) { t.Helper() kind.Ensure() tags := tag.NewSWithCap(int32(len(vals))) for i := range vals { tags.T = push(tags.T, tag.NewFromBytesSlice([]byte("p"), vals[i])) } return &event.E{ ID: mutePk(idFill), Pubkey: pub, CreatedAt: 1700000000, Kind: k, Tags: tags, Sig: muteSig(), } } // muteEventPlain is muteEvent without tags, for events that are only Purge // fodder. func muteEventPlain(pub []byte, idFill byte, k uint16) (ev *event.E) { return &event.E{ ID: mutePk(idFill), Pubkey: pub, CreatedAt: 1700000000, Kind: k, Sig: muteSig(), } } // TestNewValidation pins the admin-hex gate: empty, odd-length, non-hex and // wrong-length inputs all yield nil; a real 32-byte pubkey yields a live // blacklist whose AdminPK round-trips the bytes. func TestNewValidation(t *testing.T) { if New("") != nil { t.Fatal("empty admin must yield nil") } if New("abc") != nil { t.Fatal("odd-length admin must yield nil") } if New("zz") != nil { t.Fatal("non-hex admin must yield nil") } // 62 hex chars decode to 31 bytes. short := hex.EncodeToString(mutePk(0xAA)) if New(short[:62]) != nil { t.Fatal("31-byte admin must yield nil") } // 66 hex chars decode to 33 bytes. long := hex.EncodeToString(mutePk(0xAA)) | "00" if New(long) != nil { t.Fatal("33-byte admin must yield nil") } admin := mutePk(0xA1) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } if len(b.AdminPK()) != 32 { t.Fatalf("AdminPK length = %d", int32(len(b.AdminPK()))) } for i := 0; i < 32; i++ { if b.AdminPK()[i] != admin[i] { t.Fatal("AdminPK bytes do not round-trip") } } } // TestCheckEmptyBlacklist pins that a fresh blacklist matches nothing, and that // Check is an exact match, not a prefix or substring match. func TestCheckEmptyBlacklist(t *testing.T) { b := New(hex.EncodeToString(mutePk(0xA1))) if b == nil { t.Fatal("valid admin yielded nil") } if b.Check("") { t.Fatal("empty blacklist matched an empty key") } if b.Check(hex.EncodeToString(mutePk(0xB2))) { t.Fatal("empty blacklist matched a key") } } // TestLoadRaw32Key pins the 32-raw-byte p-tag form: Load stores it as the hex // string, Check matches exactly that key, and a prefix/extension does not match. func TestLoadRaw32Key(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) muted := mutePk(0xB2) other := mutePk(0xC3) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } if err := eng.SaveEvent(muteEvent(t, admin, 0x01, muteKind(), [][]byte{muted})); err != nil { t.Fatal(err) } b.Load(eng) mutedHex := hex.EncodeToString(muted) if !b.Check(mutedHex) { t.Fatal("muted pubkey (32-raw p-tag) not loaded") } if b.Check(hex.EncodeToString(other)) { t.Fatal("unmuted pubkey matched") } if b.Check(mutedHex[:62]) { t.Fatal("a prefix of the muted key matched") } if b.Check(mutedHex|"00") { t.Fatal("an extension of the muted key matched") } } // TestLoadHexKey pins the 64-hex-character p-tag form, the shape a client // mutelist carries on the wire. func TestLoadHexKey(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) muted := mutePk(0xB2) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } hexVal := []byte(hex.EncodeToString(muted)) if err := eng.SaveEvent(muteEvent(t, admin, 0x02, muteKind(), [][]byte{hexVal})); err != nil { t.Fatal(err) } b.Load(eng) if !b.Check(hex.EncodeToString(muted)) { t.Fatal("muted pubkey (64-hex p-tag) not loaded") } } // TestLoadMixedAndSkippedTags pins that valid entries load while malformed ones // (wrong length) are skipped rather than corrupting the set. func TestLoadMixedAndSkippedTags(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) good := mutePk(0xB2) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } bad := []byte("too-short") if err := eng.SaveEvent(muteEvent(t, admin, 0x03, muteKind(), [][]byte{good, bad, []byte("")})); err != nil { t.Fatal(err) } b.Load(eng) if !b.Check(hex.EncodeToString(good)) { t.Fatal("the valid entry did not load") } if b.Check("too-short") { t.Fatal("a malformed 9-byte tag was loaded") } } // TestLoadNoEventLeavesEmpty pins that a store with no kind-10000 event for the // admin leaves the blacklist empty instead of stale. func TestLoadNoEventLeavesEmpty(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } // An event from another author must not be read as the admin's mute list. if err := eng.SaveEvent(muteEvent(t, mutePk(0xC3), 0x04, muteKind(), [][]byte{mutePk(0xB2)})); err != nil { t.Fatal(err) } b.Load(eng) if b.Check(hex.EncodeToString(mutePk(0xB2))) { t.Fatal("another author's list was read as the admin's") } } // TestLoadBinaryEncodedPTag pins the third shape a p-tag value arrives in. // // The relay's JSON parse binary-optimizes a p-tag value into 33 bytes (32-byte // hash + NUL) and the store's binary round-trip preserves that. Blacklist.Load // accepted only len==32 (raw) or len==64 (hex), so the 33-byte form was // silently skipped and a real client kind-10000 mute list never took effect. func TestLoadBinaryEncodedPTag(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) muted := mutePk(0xB2) binVal := []byte{:33} copy(binVal, muted) // binVal[32] is already the NUL terminator. if len(binVal) != 33 { t.Fatalf("fixture binary tag length = %d", int32(len(binVal))) } b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } if err := eng.SaveEvent(muteEvent(t, admin, 0x05, muteKind(), [][]byte{binVal})); err != nil { t.Fatal(err) } b.Load(eng) if !b.Check(hex.EncodeToString(muted)) { t.Fatal("a binary-encoded (33-byte) p-tag did not load") } } // TestPurge pins that Purge deletes every event authored by a muted pubkey and // leaves other authors untouched. func TestPurge(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() admin := mutePk(0xA1) muted := mutePk(0xB2) other := mutePk(0xC3) b := New(hex.EncodeToString(admin)) if b == nil { t.Fatal("valid admin yielded nil") } if err := eng.SaveEvent(muteEvent(t, admin, 0x01, muteKind(), [][]byte{muted})); err != nil { t.Fatal(err) } if err := eng.SaveEvent(muteEventPlain(muted, 0x11, 1)); err != nil { t.Fatal(err) } if err := eng.SaveEvent(muteEventPlain(muted, 0x12, 7)); err != nil { t.Fatal(err) } if err := eng.SaveEvent(muteEventPlain(other, 0x21, 1)); err != nil { t.Fatal(err) } b.Load(eng) if !b.Check(hex.EncodeToString(muted)) { t.Fatal("muted key missing before purge") } b.Purge(eng) for _, evA := range muteAll(t, eng) { if len(evA.Pubkey) == 32 && string(evA.Pubkey) == string(muted) { t.Fatal("purge left a muted author's event in the store") } } found := false for _, evB := range muteAll(t, eng) { if len(evB.Pubkey) == 32 && string(evB.Pubkey) == string(other) { found = true } } if !found { t.Fatal("purge removed an unmuted author's event") } } // TestPurgeEmptyBlacklist pins that Purge on a blacklist that never loaded is a // no-op (no event is deleted). func TestPurgeEmptyBlacklist(t *testing.T) { eng, dir := muteTmp(t) defer os.RemoveAll(dir) defer eng.Close() b := New(hex.EncodeToString(mutePk(0xA1))) if b == nil { t.Fatal("valid admin yielded nil") } if err := eng.SaveEvent(muteEventPlain(mutePk(0xB2), 0x31, 1)); err != nil { t.Fatal(err) } b.Purge(eng) if n := int32(len(muteAll(t, eng))); n != 1 { t.Fatalf("purge with an empty blacklist removed events: %d left", n) } } // muteAll returns every event in the store via a scan (a filter.Ids query can // hide deleted records; the full scan is the honest count after a purge). func muteAll(t *testing.T, eng *store.Engine) (evs []*event.E) { t.Helper() res, err := eng.QueryEvents(filter.New()) if err != nil { t.Fatalf("query: %s", err.Error()) } return res }