mute_test.mx raw

   1  package mute
   2  
   3  import (
   4  	"encoding/hex"
   5  	"os"
   6  	"testing"
   7  
   8  	"git.smesh.lol/smesh/pkg/nostr/event"
   9  	"git.smesh.lol/smesh/pkg/nostr/filter"
  10  	"git.smesh.lol/smesh/pkg/nostr/kind"
  11  	"git.smesh.lol/smesh/pkg/nostr/tag"
  12  	"git.smesh.lol/smesh/pkg/store"
  13  )
  14  
  15  // mutePk is a fixed 32-byte pubkey so the store round-trip needs no signer.
  16  func mutePk(fill byte) (b []byte) {
  17  	b = []byte{:32}
  18  	for i := range b {
  19  		b[i] = fill
  20  	}
  21  	return
  22  }
  23  
  24  func muteSig() (b []byte) {
  25  	b = []byte{:64}
  26  	for i := range b {
  27  		b[i] = byte(i)
  28  	}
  29  	return
  30  }
  31  
  32  func muteTmp(t *testing.T) (eng *store.Engine, dir string) {
  33  	t.Helper()
  34  	dir, derr := os.MkdirTemp("", "moxie-mute")
  35  	if derr != nil {
  36  		t.Fatal(derr)
  37  	}
  38  	eng, oerr := store.Open(dir)
  39  	if oerr != nil {
  40  		t.Fatal(oerr)
  41  	}
  42  	return eng, dir
  43  }
  44  
  45  // muteKind materializes the kind table before reading the package pointer:
  46  // kind.MuteList is nil until kind.Ensure() runs.
  47  func muteKind() (k uint16) {
  48  	kind.Ensure()
  49  	return kind.MuteList.K
  50  }
  51  
  52  // muteEvent builds an event with one p-tag per raw value. The caller chooses
  53  // the value form: 32 raw bytes, 64 hex chars, or the 33-byte binary form.
  54  func muteEvent(t *testing.T, pub []byte, idFill byte, k uint16, vals [][]byte) (ev *event.E) {
  55  	t.Helper()
  56  	kind.Ensure()
  57  	tags := tag.NewSWithCap(int32(len(vals)))
  58  	for i := range vals {
  59  		tags.T = push(tags.T, tag.NewFromBytesSlice([]byte("p"), vals[i]))
  60  	}
  61  	return &event.E{
  62  		ID:        mutePk(idFill),
  63  		Pubkey:    pub,
  64  		CreatedAt: 1700000000,
  65  		Kind:      k,
  66  		Tags:      tags,
  67  		Sig:       muteSig(),
  68  	}
  69  }
  70  
  71  // muteEventPlain is muteEvent without tags, for events that are only Purge
  72  // fodder.
  73  func muteEventPlain(pub []byte, idFill byte, k uint16) (ev *event.E) {
  74  	return &event.E{
  75  		ID:        mutePk(idFill),
  76  		Pubkey:    pub,
  77  		CreatedAt: 1700000000,
  78  		Kind:      k,
  79  		Sig:       muteSig(),
  80  	}
  81  }
  82  
  83  // TestNewValidation pins the admin-hex gate: empty, odd-length, non-hex and
  84  // wrong-length inputs all yield nil; a real 32-byte pubkey yields a live
  85  // blacklist whose AdminPK round-trips the bytes.
  86  func TestNewValidation(t *testing.T) {
  87  	if New("") != nil {
  88  		t.Fatal("empty admin must yield nil")
  89  	}
  90  	if New("abc") != nil {
  91  		t.Fatal("odd-length admin must yield nil")
  92  	}
  93  	if New("zz") != nil {
  94  		t.Fatal("non-hex admin must yield nil")
  95  	}
  96  	// 62 hex chars decode to 31 bytes.
  97  	short := hex.EncodeToString(mutePk(0xAA))
  98  	if New(short[:62]) != nil {
  99  		t.Fatal("31-byte admin must yield nil")
 100  	}
 101  	// 66 hex chars decode to 33 bytes.
 102  	long := hex.EncodeToString(mutePk(0xAA)) | "00"
 103  	if New(long) != nil {
 104  		t.Fatal("33-byte admin must yield nil")
 105  	}
 106  
 107  	admin := mutePk(0xA1)
 108  	b := New(hex.EncodeToString(admin))
 109  	if b == nil {
 110  		t.Fatal("valid admin yielded nil")
 111  	}
 112  	if len(b.AdminPK()) != 32 {
 113  		t.Fatalf("AdminPK length = %d", int32(len(b.AdminPK())))
 114  	}
 115  	for i := 0; i < 32; i++ {
 116  		if b.AdminPK()[i] != admin[i] {
 117  			t.Fatal("AdminPK bytes do not round-trip")
 118  		}
 119  	}
 120  }
 121  
 122  // TestCheckEmptyBlacklist pins that a fresh blacklist matches nothing, and that
 123  // Check is an exact match, not a prefix or substring match.
 124  func TestCheckEmptyBlacklist(t *testing.T) {
 125  	b := New(hex.EncodeToString(mutePk(0xA1)))
 126  	if b == nil {
 127  		t.Fatal("valid admin yielded nil")
 128  	}
 129  	if b.Check("") {
 130  		t.Fatal("empty blacklist matched an empty key")
 131  	}
 132  	if b.Check(hex.EncodeToString(mutePk(0xB2))) {
 133  		t.Fatal("empty blacklist matched a key")
 134  	}
 135  }
 136  
 137  // TestLoadRaw32Key pins the 32-raw-byte p-tag form: Load stores it as the hex
 138  // string, Check matches exactly that key, and a prefix/extension does not match.
 139  func TestLoadRaw32Key(t *testing.T) {
 140  	eng, dir := muteTmp(t)
 141  	defer os.RemoveAll(dir)
 142  	defer eng.Close()
 143  
 144  	admin := mutePk(0xA1)
 145  	muted := mutePk(0xB2)
 146  	other := mutePk(0xC3)
 147  	b := New(hex.EncodeToString(admin))
 148  	if b == nil {
 149  		t.Fatal("valid admin yielded nil")
 150  	}
 151  	if err := eng.SaveEvent(muteEvent(t, admin, 0x01, muteKind(), [][]byte{muted})); err != nil {
 152  		t.Fatal(err)
 153  	}
 154  	b.Load(eng)
 155  
 156  	mutedHex := hex.EncodeToString(muted)
 157  	if !b.Check(mutedHex) {
 158  		t.Fatal("muted pubkey (32-raw p-tag) not loaded")
 159  	}
 160  	if b.Check(hex.EncodeToString(other)) {
 161  		t.Fatal("unmuted pubkey matched")
 162  	}
 163  	if b.Check(mutedHex[:62]) {
 164  		t.Fatal("a prefix of the muted key matched")
 165  	}
 166  	if b.Check(mutedHex|"00") {
 167  		t.Fatal("an extension of the muted key matched")
 168  	}
 169  }
 170  
 171  // TestLoadHexKey pins the 64-hex-character p-tag form, the shape a client
 172  // mutelist carries on the wire.
 173  func TestLoadHexKey(t *testing.T) {
 174  	eng, dir := muteTmp(t)
 175  	defer os.RemoveAll(dir)
 176  	defer eng.Close()
 177  
 178  	admin := mutePk(0xA1)
 179  	muted := mutePk(0xB2)
 180  	b := New(hex.EncodeToString(admin))
 181  	if b == nil {
 182  		t.Fatal("valid admin yielded nil")
 183  	}
 184  	hexVal := []byte(hex.EncodeToString(muted))
 185  	if err := eng.SaveEvent(muteEvent(t, admin, 0x02, muteKind(), [][]byte{hexVal})); err != nil {
 186  		t.Fatal(err)
 187  	}
 188  	b.Load(eng)
 189  	if !b.Check(hex.EncodeToString(muted)) {
 190  		t.Fatal("muted pubkey (64-hex p-tag) not loaded")
 191  	}
 192  }
 193  
 194  // TestLoadMixedAndSkippedTags pins that valid entries load while malformed ones
 195  // (wrong length) are skipped rather than corrupting the set.
 196  func TestLoadMixedAndSkippedTags(t *testing.T) {
 197  	eng, dir := muteTmp(t)
 198  	defer os.RemoveAll(dir)
 199  	defer eng.Close()
 200  
 201  	admin := mutePk(0xA1)
 202  	good := mutePk(0xB2)
 203  	b := New(hex.EncodeToString(admin))
 204  	if b == nil {
 205  		t.Fatal("valid admin yielded nil")
 206  	}
 207  	bad := []byte("too-short")
 208  	if err := eng.SaveEvent(muteEvent(t, admin, 0x03, muteKind(),
 209  		[][]byte{good, bad, []byte("")})); err != nil {
 210  		t.Fatal(err)
 211  	}
 212  	b.Load(eng)
 213  	if !b.Check(hex.EncodeToString(good)) {
 214  		t.Fatal("the valid entry did not load")
 215  	}
 216  	if b.Check("too-short") {
 217  		t.Fatal("a malformed 9-byte tag was loaded")
 218  	}
 219  }
 220  
 221  // TestLoadNoEventLeavesEmpty pins that a store with no kind-10000 event for the
 222  // admin leaves the blacklist empty instead of stale.
 223  func TestLoadNoEventLeavesEmpty(t *testing.T) {
 224  	eng, dir := muteTmp(t)
 225  	defer os.RemoveAll(dir)
 226  	defer eng.Close()
 227  
 228  	admin := mutePk(0xA1)
 229  	b := New(hex.EncodeToString(admin))
 230  	if b == nil {
 231  		t.Fatal("valid admin yielded nil")
 232  	}
 233  	// An event from another author must not be read as the admin's mute list.
 234  	if err := eng.SaveEvent(muteEvent(t, mutePk(0xC3), 0x04, muteKind(), [][]byte{mutePk(0xB2)})); err != nil {
 235  		t.Fatal(err)
 236  	}
 237  	b.Load(eng)
 238  	if b.Check(hex.EncodeToString(mutePk(0xB2))) {
 239  		t.Fatal("another author's list was read as the admin's")
 240  	}
 241  }
 242  
 243  // TestLoadBinaryEncodedPTag pins the third shape a p-tag value arrives in.
 244  //
 245  // The relay's JSON parse binary-optimizes a p-tag value into 33 bytes (32-byte
 246  // hash + NUL) and the store's binary round-trip preserves that. Blacklist.Load
 247  // accepted only len==32 (raw) or len==64 (hex), so the 33-byte form was
 248  // silently skipped and a real client kind-10000 mute list never took effect.
 249  func TestLoadBinaryEncodedPTag(t *testing.T) {
 250  	eng, dir := muteTmp(t)
 251  	defer os.RemoveAll(dir)
 252  	defer eng.Close()
 253  
 254  	admin := mutePk(0xA1)
 255  	muted := mutePk(0xB2)
 256  	binVal := []byte{:33}
 257  	copy(binVal, muted)
 258  	// binVal[32] is already the NUL terminator.
 259  	if len(binVal) != 33 {
 260  		t.Fatalf("fixture binary tag length = %d", int32(len(binVal)))
 261  	}
 262  	b := New(hex.EncodeToString(admin))
 263  	if b == nil {
 264  		t.Fatal("valid admin yielded nil")
 265  	}
 266  	if err := eng.SaveEvent(muteEvent(t, admin, 0x05, muteKind(), [][]byte{binVal})); err != nil {
 267  		t.Fatal(err)
 268  	}
 269  	b.Load(eng)
 270  	if !b.Check(hex.EncodeToString(muted)) {
 271  		t.Fatal("a binary-encoded (33-byte) p-tag did not load")
 272  	}
 273  }
 274  
 275  // TestPurge pins that Purge deletes every event authored by a muted pubkey and
 276  // leaves other authors untouched.
 277  func TestPurge(t *testing.T) {
 278  	eng, dir := muteTmp(t)
 279  	defer os.RemoveAll(dir)
 280  	defer eng.Close()
 281  
 282  	admin := mutePk(0xA1)
 283  	muted := mutePk(0xB2)
 284  	other := mutePk(0xC3)
 285  	b := New(hex.EncodeToString(admin))
 286  	if b == nil {
 287  		t.Fatal("valid admin yielded nil")
 288  	}
 289  	if err := eng.SaveEvent(muteEvent(t, admin, 0x01, muteKind(), [][]byte{muted})); err != nil {
 290  		t.Fatal(err)
 291  	}
 292  	if err := eng.SaveEvent(muteEventPlain(muted, 0x11, 1)); err != nil {
 293  		t.Fatal(err)
 294  	}
 295  	if err := eng.SaveEvent(muteEventPlain(muted, 0x12, 7)); err != nil {
 296  		t.Fatal(err)
 297  	}
 298  	if err := eng.SaveEvent(muteEventPlain(other, 0x21, 1)); err != nil {
 299  		t.Fatal(err)
 300  	}
 301  	b.Load(eng)
 302  	if !b.Check(hex.EncodeToString(muted)) {
 303  		t.Fatal("muted key missing before purge")
 304  	}
 305  	b.Purge(eng)
 306  
 307  	for _, evA := range muteAll(t, eng) {
 308  		if len(evA.Pubkey) == 32 && string(evA.Pubkey) == string(muted) {
 309  			t.Fatal("purge left a muted author's event in the store")
 310  		}
 311  	}
 312  	found := false
 313  	for _, evB := range muteAll(t, eng) {
 314  		if len(evB.Pubkey) == 32 && string(evB.Pubkey) == string(other) {
 315  			found = true
 316  		}
 317  	}
 318  	if !found {
 319  		t.Fatal("purge removed an unmuted author's event")
 320  	}
 321  }
 322  
 323  // TestPurgeEmptyBlacklist pins that Purge on a blacklist that never loaded is a
 324  // no-op (no event is deleted).
 325  func TestPurgeEmptyBlacklist(t *testing.T) {
 326  	eng, dir := muteTmp(t)
 327  	defer os.RemoveAll(dir)
 328  	defer eng.Close()
 329  
 330  	b := New(hex.EncodeToString(mutePk(0xA1)))
 331  	if b == nil {
 332  		t.Fatal("valid admin yielded nil")
 333  	}
 334  	if err := eng.SaveEvent(muteEventPlain(mutePk(0xB2), 0x31, 1)); err != nil {
 335  		t.Fatal(err)
 336  	}
 337  	b.Purge(eng)
 338  	if n := int32(len(muteAll(t, eng))); n != 1 {
 339  		t.Fatalf("purge with an empty blacklist removed events: %d left", n)
 340  	}
 341  }
 342  
 343  // muteAll returns every event in the store via a scan (a filter.Ids query can
 344  // hide deleted records; the full scan is the honest count after a purge).
 345  func muteAll(t *testing.T, eng *store.Engine) (evs []*event.E) {
 346  	t.Helper()
 347  	res, err := eng.QueryEvents(filter.New())
 348  	if err != nil {
 349  		t.Fatalf("query: %s", err.Error())
 350  	}
 351  	return res
 352  }
 353