acl_test.mx raw

   1  package acl
   2  
   3  import (
   4  	"bytes"
   5  	"encoding/hex"
   6  	"os"
   7  	"testing"
   8  	"time"
   9  
  10  	"git.smesh.lol/nostr/pkg/event"
  11  	"git.smesh.lol/nostr/pkg/kind"
  12  	"git.smesh.lol/nostr/pkg/tag"
  13  	"git.smesh.lol/morly/pkg/store"
  14  )
  15  
  16  // aclPk is a fixed 32-byte pubkey so the store round-trip needs no signer.
  17  func aclPk(fill byte) (b []byte) {
  18  	b = []byte{:32}
  19  	for i := range b {
  20  		b[i] = fill
  21  	}
  22  	return
  23  }
  24  
  25  func aclSig() (b []byte) {
  26  	b = []byte{:64}
  27  	for i := range b {
  28  		b[i] = byte(i)
  29  	}
  30  	return
  31  }
  32  
  33  // aclTmp opens a store in a fresh directory. The caller owns both.
  34  func aclTmp(t *testing.T) (eng *store.Engine, dir string) {
  35  	t.Helper()
  36  	dir, derr := os.MkdirTemp("", "moxie-acl")
  37  	if derr != nil {
  38  		t.Fatal(derr)
  39  	}
  40  	eng, oerr := store.Open(dir)
  41  	if oerr != nil {
  42  		t.Fatal(oerr)
  43  	}
  44  	return eng, dir
  45  }
  46  
  47  // aclBinTag is the 33-byte binary form grapevine.GetFollows expects
  48  // (ValueBinary strips the trailing NUL).
  49  func aclBinTag(pk []byte) (b []byte) {
  50  	b = []byte{:33}
  51  	copy(b, pk)
  52  	return
  53  }
  54  
  55  // aclFollowList builds a kind-3 event authored by admin that follows one
  56  // pubkey. idFill keeps two events in one store from colliding.
  57  func aclFollowList(t *testing.T, admin []byte, idFill byte, followed []byte) (ev *event.E) {
  58  	t.Helper()
  59  	kind.Ensure()
  60  	tags := tag.NewSWithCap(1)
  61  	tags.T = push(tags.T, tag.NewFromBytesSlice([]byte("p"), aclBinTag(followed)))
  62  	return &event.E{
  63  		ID:        aclPk(idFill),
  64  		Pubkey:    admin,
  65  		CreatedAt: 1700000000,
  66  		Kind:      kind.FollowList.K,
  67  		Tags:      tags,
  68  		Sig:       aclSig(),
  69  	}
  70  }
  71  
  72  // --- acl.mx ---
  73  
  74  func TestOpenAllowsEverything(t *testing.T) {
  75  	var o Checker = &Open{}
  76  	if !o.AllowWrite(aclPk(0x01), 1) {
  77  		t.Fatal("Open must allow every write")
  78  	}
  79  	if !o.AllowRead(aclPk(0x01)) {
  80  		t.Fatal("Open must allow every read")
  81  	}
  82  	if !o.AllowWrite(nil, 0) {
  83  		t.Fatal("Open must allow an empty pubkey")
  84  	}
  85  }
  86  
  87  func TestWhitelist(t *testing.T) {
  88  	var c Checker = &Whitelist{Pubkeys: [][]byte{aclPk(0xA1), aclPk(0xB2)}}
  89  	if !c.AllowWrite(aclPk(0xA1), 1) {
  90  		t.Fatal("whitelisted pubkey 1 rejected")
  91  	}
  92  	if !c.AllowWrite(aclPk(0xB2), 7) {
  93  		t.Fatal("whitelisted pubkey 2 rejected")
  94  	}
  95  	if c.AllowWrite(aclPk(0xC3), 1) {
  96  		t.Fatal("non-whitelisted pubkey accepted")
  97  	}
  98  	if c.AllowWrite(nil, 1) {
  99  		t.Fatal("nil pubkey accepted")
 100  	}
 101  	if !c.AllowRead(aclPk(0xC3)) {
 102  		t.Fatal("Whitelist must allow every read")
 103  	}
 104  
 105  	empty := &Whitelist{}
 106  	if empty.AllowWrite(aclPk(0xA1), 1) {
 107  		t.Fatal("empty whitelist accepted a write")
 108  	}
 109  }
 110  
 111  func TestReadOnly(t *testing.T) {
 112  	var c Checker = &ReadOnly{}
 113  	if c.AllowWrite(aclPk(0xA1), 1) {
 114  		t.Fatal("ReadOnly accepted a write")
 115  	}
 116  	if c.AllowWrite(nil, 0) {
 117  		t.Fatal("ReadOnly accepted an empty write")
 118  	}
 119  	if !c.AllowRead(aclPk(0xA1)) {
 120  		t.Fatal("ReadOnly must allow reads")
 121  	}
 122  }
 123  
 124  // --- follows.mx ---
 125  
 126  func TestHexDec(t *testing.T) {
 127  	if empty := hexDec(""); len(empty) != 0 {
 128  		t.Fatalf("hexDec(\"\") length = %d", int32(len(empty)))
 129  	}
 130  	dec := hexDec("00ff10aF")
 131  	if len(dec) != 4 {
 132  		t.Fatalf("hexDec length = %d", int32(len(dec)))
 133  	}
 134  	if dec[0] != 0x00 || dec[1] != 0xff || dec[2] != 0x10 || dec[3] != 0xaf {
 135  		t.Fatalf("hexDec bytes = %x", dec)
 136  	}
 137  	if hexDec("0") != nil {
 138  		t.Fatal("odd-length hex accepted")
 139  	}
 140  	if hexDec("zz") != nil {
 141  		t.Fatal("non-hex accepted")
 142  	}
 143  	if hexDec("0g") != nil {
 144  		t.Fatal("partially non-hex accepted")
 145  	}
 146  }
 147  
 148  func TestUnhex(t *testing.T) {
 149  	if unhex('0') != 0 {
 150  		t.Fatal("unhex('0')")
 151  	}
 152  	if unhex('9') != 9 {
 153  		t.Fatal("unhex('9')")
 154  	}
 155  	if unhex('a') != 10 {
 156  		t.Fatal("unhex('a')")
 157  	}
 158  	if unhex('f') != 15 {
 159  		t.Fatal("unhex('f')")
 160  	}
 161  	if unhex('A') != 10 {
 162  		t.Fatal("unhex('A')")
 163  	}
 164  	if unhex('F') != 15 {
 165  		t.Fatal("unhex('F')")
 166  	}
 167  	if unhex('g') != 0xff {
 168  		t.Fatal("unhex('g') must be the invalid sentinel")
 169  	}
 170  	if unhex('/') != 0xff {
 171  		t.Fatal("unhex('/') must be the invalid sentinel")
 172  	}
 173  }
 174  
 175  // TestFollowsWithStore pins the store-backed decision: the admin can always
 176  // write, a pubkey on the admin's kind-3 follow list can write, and everyone
 177  // else is denied.
 178  func TestFollowsWithStore(t *testing.T) {
 179  	eng, dir := aclTmp(t)
 180  	defer os.RemoveAll(dir)
 181  	defer eng.Close()
 182  
 183  	admin := aclPk(0xA1)
 184  	alice := aclPk(0xB2)
 185  	bob := aclPk(0xC3)
 186  	if err := eng.SaveEvent(aclFollowList(t, admin, 0xE1, alice)); err != nil {
 187  		t.Fatal(err)
 188  	}
 189  
 190  	f := NewFollows(eng, []string{hex.EncodeToString(admin)}, 3600)
 191  	if !f.AllowWrite(admin, 1) {
 192  		t.Fatal("admin must always write")
 193  	}
 194  	if !f.AllowWrite(alice, 1) {
 195  		t.Fatal("followed pubkey must write")
 196  	}
 197  	if f.AllowWrite(bob, 1) {
 198  		t.Fatal("unfollowed pubkey must not write")
 199  	}
 200  	if !f.AllowRead(bob) {
 201  		t.Fatal("Follows must allow every read")
 202  	}
 203  	if !f.IsFollowed(alice) {
 204  		t.Fatal("IsFollowed(alice)")
 205  	}
 206  	if f.IsFollowed(bob) {
 207  		t.Fatal("IsFollowed(bob)")
 208  	}
 209  	// refresh() seeds the admin set into `followed` as well, so the admin is
 210  	// reported as followed.
 211  	if !f.IsFollowed(admin) {
 212  		t.Fatal("the admin must be seeded into the followed map")
 213  	}
 214  }
 215  
 216  // TestFollowsAdminParsing pins that only well-formed 32-byte hex admins are
 217  // kept, and that a valid admin entry still works alongside malformed ones.
 218  func TestFollowsAdminParsing(t *testing.T) {
 219  	eng, dir := aclTmp(t)
 220  	defer os.RemoveAll(dir)
 221  	defer eng.Close()
 222  
 223  	admin := aclPk(0xA1)
 224  	alice := aclPk(0xB2)
 225  	if err := eng.SaveEvent(aclFollowList(t, admin, 0xE1, alice)); err != nil {
 226  		t.Fatal(err)
 227  	}
 228  
 229  	// empty, odd-length, non-hex and short-but-even entries are all dropped.
 230  	f := NewFollows(eng, []string{"", "abc", "zz", "abcd", hex.EncodeToString(admin)}, 3600)
 231  	if len(f.admins) != 1 {
 232  		t.Fatalf("admins kept = %d, want 1", int32(len(f.admins)))
 233  	}
 234  	if !bytes.Equal(f.admins[0], admin) {
 235  		t.Fatal("the surviving admin is not the valid one")
 236  	}
 237  	if !f.AllowWrite(alice, 1) {
 238  		t.Fatal("the valid admin's follow list was not loaded")
 239  	}
 240  }
 241  
 242  // TestFollowsWithoutStore drives AllowWrite/IsFollowed on a constructed value
 243  // with no store: refresh is skipped, so the decision comes from the in-memory
 244  // maps alone. This is the store-free half of the API.
 245  func TestFollowsWithoutStore(t *testing.T) {
 246  	admin := aclPk(0xA1)
 247  	alice := aclPk(0xB2)
 248  	bob := aclPk(0xC3)
 249  	f := &Follows{
 250  		admins:      [][]byte{admin},
 251  		followed:    map[string]bool{string(alice): true},
 252  		freqSec:     3600,
 253  		lastRefresh: time.Now().Unix(),
 254  	}
 255  	if !f.AllowWrite(admin, 1) {
 256  		t.Fatal("admin must write without a store")
 257  	}
 258  	if !f.AllowWrite(alice, 1) {
 259  		t.Fatal("followed pubkey must write without a store")
 260  	}
 261  	if f.AllowWrite(bob, 1) {
 262  		t.Fatal("unknown pubkey must not write without a store")
 263  	}
 264  	if !f.AllowRead(bob) {
 265  		t.Fatal("AllowRead must be unconditional")
 266  	}
 267  	if !f.IsFollowed(alice) || f.IsFollowed(bob) {
 268  		t.Fatal("IsFollowed without a store")
 269  	}
 270  }
 271  
 272  // --- social.mx ---
 273  
 274  // TestSocialWithStore pins the WoT-depth decisions at maxDepth 2:
 275  // admin=0, direct follow=1, follow-of-follow=2, outsider=-1.
 276  func TestSocialWithStore(t *testing.T) {
 277  	eng, dir := aclTmp(t)
 278  	defer os.RemoveAll(dir)
 279  	defer eng.Close()
 280  
 281  	admin := aclPk(0xA1)
 282  	alice := aclPk(0xB2)
 283  	bob := aclPk(0xC3)
 284  	outsider := aclPk(0xDD)
 285  	if err := eng.SaveEvent(aclFollowList(t, admin, 0xE1, alice)); err != nil {
 286  		t.Fatal(err)
 287  	}
 288  	if err := eng.SaveEvent(aclFollowList(t, alice, 0xE2, bob)); err != nil {
 289  		t.Fatal(err)
 290  	}
 291  
 292  	s := NewSocial(eng, []string{hex.EncodeToString(admin)}, 2, 3600)
 293  	if s.Depth(admin) != 0 {
 294  		t.Fatalf("Depth(admin) = %d, want 0", s.Depth(admin))
 295  	}
 296  	if s.Depth(alice) != 1 {
 297  		t.Fatalf("Depth(alice) = %d, want 1", s.Depth(alice))
 298  	}
 299  	if s.Depth(bob) != 2 {
 300  		t.Fatalf("Depth(bob) = %d, want 2", s.Depth(bob))
 301  	}
 302  	if s.Depth(outsider) != -1 {
 303  		t.Fatalf("Depth(outsider) = %d, want -1", s.Depth(outsider))
 304  	}
 305  	if !s.AllowWrite(admin, 1) {
 306  		t.Fatal("admin must write")
 307  	}
 308  	if !s.AllowWrite(alice, 1) {
 309  		t.Fatal("depth-1 pubkey must write")
 310  	}
 311  	if !s.AllowWrite(bob, 1) {
 312  		t.Fatal("depth-2 pubkey must write")
 313  	}
 314  	if s.AllowWrite(outsider, 1) {
 315  		t.Fatal("depth -1 pubkey must not write")
 316  	}
 317  	if !s.AllowRead(outsider) {
 318  		t.Fatal("Social must allow every read")
 319  	}
 320  }
 321  
 322  // TestSocialRespectsMaxDepth pins that maxDepth truncates the traversal: at
 323  // maxDepth 1 the second hop is unknown (depth -1) and cannot write.
 324  func TestSocialRespectsMaxDepth(t *testing.T) {
 325  	eng, dir := aclTmp(t)
 326  	defer os.RemoveAll(dir)
 327  	defer eng.Close()
 328  
 329  	admin := aclPk(0xA1)
 330  	alice := aclPk(0xB2)
 331  	bob := aclPk(0xC3)
 332  	if err := eng.SaveEvent(aclFollowList(t, admin, 0xE1, alice)); err != nil {
 333  		t.Fatal(err)
 334  	}
 335  	if err := eng.SaveEvent(aclFollowList(t, alice, 0xE2, bob)); err != nil {
 336  		t.Fatal(err)
 337  	}
 338  
 339  	s := NewSocial(eng, []string{hex.EncodeToString(admin)}, 1, 3600)
 340  	if s.Depth(alice) != 1 {
 341  		t.Fatalf("Depth(alice) = %d, want 1", s.Depth(alice))
 342  	}
 343  	if s.Depth(bob) != -1 {
 344  		t.Fatalf("Depth(bob) at maxDepth 1 = %d, want -1", s.Depth(bob))
 345  	}
 346  	if s.AllowWrite(bob, 1) {
 347  		t.Fatal("beyond maxDepth must not write")
 348  	}
 349  }
 350  
 351  // TestSocialWithoutStore drives the store-free half: Depth and AllowWrite read
 352  // the constructed depthMap and admin list directly.
 353  func TestSocialWithoutStore(t *testing.T) {
 354  	admin := aclPk(0xA1)
 355  	alice := aclPk(0xB2)
 356  	bob := aclPk(0xC3)
 357  	s := &Social{
 358  		admins:      [][]byte{admin},
 359  		maxDepth:    2,
 360  		refreshSec:  3600,
 361  		lastRefresh: time.Now().Unix(),
 362  		depthMap:    map[string]int32{string(alice): 1},
 363  	}
 364  	if s.Depth(admin) != 0 {
 365  		t.Fatal("admin depth must be 0 without a store")
 366  	}
 367  	if s.Depth(alice) != 1 {
 368  		t.Fatal("mapped depth must be returned")
 369  	}
 370  	if s.Depth(bob) != -1 {
 371  		t.Fatal("unmapped depth must be -1")
 372  	}
 373  	if !s.AllowWrite(admin, 1) || !s.AllowWrite(alice, 1) {
 374  		t.Fatal("admin and mapped pubkey must write")
 375  	}
 376  	if s.AllowWrite(bob, 1) {
 377  		t.Fatal("unmapped pubkey must not write")
 378  	}
 379  	if !s.AllowRead(bob) {
 380  		t.Fatal("AllowRead must be unconditional")
 381  	}
 382  }
 383