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