implemented and tested NIP-43 invite based ACL

This commit is contained in:
2025-11-09 10:41:58 +00:00
parent f0beb83ceb
commit d0dbd2e2dc
25 changed files with 2958 additions and 203 deletions

570
app/handle-nip43_test.go Normal file
View File

@@ -0,0 +1,570 @@
package app
import (
"context"
"os"
"testing"
"time"
"next.orly.dev/app/config"
"next.orly.dev/pkg/crypto/keys"
"next.orly.dev/pkg/database"
"next.orly.dev/pkg/encoders/event"
"next.orly.dev/pkg/encoders/tag"
"next.orly.dev/pkg/interfaces/signer/p8k"
"next.orly.dev/pkg/protocol/nip43"
"next.orly.dev/pkg/protocol/publish"
)
// setupTestListener creates a test listener with NIP-43 enabled
func setupTestListener(t *testing.T) (*Listener, *database.D, func()) {
tempDir, err := os.MkdirTemp("", "nip43_handler_test_*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
db, err := database.New(ctx, cancel, tempDir, "info")
if err != nil {
os.RemoveAll(tempDir)
t.Fatalf("failed to open database: %v", err)
}
cfg := &config.C{
NIP43Enabled: true,
NIP43PublishEvents: true,
NIP43PublishMemberList: true,
NIP43InviteExpiry: 24 * time.Hour,
RelayURL: "wss://test.relay",
Listen: "localhost",
Port: 3334,
}
server := &Server{
Ctx: ctx,
Config: cfg,
D: db,
publishers: publish.New(NewPublisher(ctx)),
InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry),
cfg: cfg,
db: db,
}
listener := &Listener{
Server: server,
ctx: ctx,
}
cleanup := func() {
db.Close()
os.RemoveAll(tempDir)
}
return listener, db, cleanup
}
// TestHandleNIP43JoinRequest_ValidRequest tests a successful join request
func TestHandleNIP43JoinRequest_ValidRequest(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Generate invite code
code, err := listener.Server.InviteManager.GenerateCode()
if err != nil {
t.Fatalf("failed to generate invite code: %v", err)
}
// Create join request event
ev := event.New()
ev.Kind = nip43.KindJoinRequest
copy(ev.Pubkey, userPubkey)
ev.Tags = tag.NewS()
ev.Tags.Append(tag.NewFromAny("-"))
ev.Tags.Append(tag.NewFromAny("claim", code))
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("")
// Sign event
if err = ev.Sign(userSigner); err != nil {
t.Fatalf("failed to sign event: %v", err)
}
// Handle join request
err = listener.HandleNIP43JoinRequest(ev)
if err != nil {
t.Fatalf("failed to handle join request: %v", err)
}
// Verify user was added to database
isMember, err := db.IsNIP43Member(userPubkey)
if err != nil {
t.Fatalf("failed to check membership: %v", err)
}
if !isMember {
t.Error("user was not added as member")
}
// Verify membership details
membership, err := db.GetNIP43Membership(userPubkey)
if err != nil {
t.Fatalf("failed to get membership: %v", err)
}
if membership.InviteCode != code {
t.Errorf("wrong invite code stored: got %s, want %s", membership.InviteCode, code)
}
}
// TestHandleNIP43JoinRequest_InvalidCode tests join request with invalid code
func TestHandleNIP43JoinRequest_InvalidCode(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Create join request with invalid code
ev := event.New()
ev.Kind = nip43.KindJoinRequest
copy(ev.Pubkey, userPubkey)
ev.Tags = tag.NewS()
ev.Tags.Append(tag.NewFromAny("-"))
ev.Tags.Append(tag.NewFromAny("claim", "invalid-code-123"))
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("")
if err = ev.Sign(userSigner); err != nil {
t.Fatalf("failed to sign event: %v", err)
}
// Handle join request - should succeed but not add member
err = listener.HandleNIP43JoinRequest(ev)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
// Verify user was NOT added
isMember, err := db.IsNIP43Member(userPubkey)
if err != nil {
t.Fatalf("failed to check membership: %v", err)
}
if isMember {
t.Error("user was incorrectly added as member with invalid code")
}
}
// TestHandleNIP43JoinRequest_DuplicateMember tests join request from existing member
func TestHandleNIP43JoinRequest_DuplicateMember(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Add user directly to database
err = db.AddNIP43Member(userPubkey, "original-code")
if err != nil {
t.Fatalf("failed to add member: %v", err)
}
// Generate new invite code
code, err := listener.Server.InviteManager.GenerateCode()
if err != nil {
t.Fatalf("failed to generate invite code: %v", err)
}
// Create join request
ev := event.New()
ev.Kind = nip43.KindJoinRequest
copy(ev.Pubkey, userPubkey)
ev.Tags = tag.NewS()
ev.Tags.Append(tag.NewFromAny("-"))
ev.Tags.Append(tag.NewFromAny("claim", code))
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("")
if err = ev.Sign(userSigner); err != nil {
t.Fatalf("failed to sign event: %v", err)
}
// Handle join request - should handle gracefully
err = listener.HandleNIP43JoinRequest(ev)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
// Verify original membership is unchanged
membership, err := db.GetNIP43Membership(userPubkey)
if err != nil {
t.Fatalf("failed to get membership: %v", err)
}
if membership.InviteCode != "original-code" {
t.Errorf("invite code was changed: got %s, want original-code", membership.InviteCode)
}
}
// TestHandleNIP43LeaveRequest_ValidRequest tests a successful leave request
func TestHandleNIP43LeaveRequest_ValidRequest(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Add user as member
err = db.AddNIP43Member(userPubkey, "test-code")
if err != nil {
t.Fatalf("failed to add member: %v", err)
}
// Create leave request
ev := event.New()
ev.Kind = nip43.KindLeaveRequest
copy(ev.Pubkey, userPubkey)
ev.Tags = tag.NewS()
ev.Tags.Append(tag.NewFromAny("-"))
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("")
if err = ev.Sign(userSigner); err != nil {
t.Fatalf("failed to sign event: %v", err)
}
// Handle leave request
err = listener.HandleNIP43LeaveRequest(ev)
if err != nil {
t.Fatalf("failed to handle leave request: %v", err)
}
// Verify user was removed
isMember, err := db.IsNIP43Member(userPubkey)
if err != nil {
t.Fatalf("failed to check membership: %v", err)
}
if isMember {
t.Error("user was not removed")
}
}
// TestHandleNIP43LeaveRequest_NonMember tests leave request from non-member
func TestHandleNIP43LeaveRequest_NonMember(t *testing.T) {
listener, _, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user (not a member)
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Create leave request
ev := event.New()
ev.Kind = nip43.KindLeaveRequest
copy(ev.Pubkey, userPubkey)
ev.Tags = tag.NewS()
ev.Tags.Append(tag.NewFromAny("-"))
ev.CreatedAt = time.Now().Unix()
ev.Content = []byte("")
if err = ev.Sign(userSigner); err != nil {
t.Fatalf("failed to sign event: %v", err)
}
// Handle leave request - should handle gracefully
err = listener.HandleNIP43LeaveRequest(ev)
if err != nil {
t.Fatalf("handler returned error: %v", err)
}
}
// TestHandleNIP43InviteRequest_ValidRequest tests invite request from admin
func TestHandleNIP43InviteRequest_ValidRequest(t *testing.T) {
listener, _, cleanup := setupTestListener(t)
defer cleanup()
// Generate admin user
adminSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate admin secret: %v", err)
}
adminSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = adminSigner.InitSec(adminSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
adminPubkey := adminSigner.Pub()
// Add admin to server (simulating admin config)
listener.Server.Admins = [][]byte{adminPubkey}
// Handle invite request
inviteEvent, err := listener.Server.HandleNIP43InviteRequest(adminPubkey)
if err != nil {
t.Fatalf("failed to handle invite request: %v", err)
}
// Verify invite event
if inviteEvent == nil {
t.Fatal("invite event is nil")
}
if inviteEvent.Kind != nip43.KindInviteReq {
t.Errorf("wrong event kind: got %d, want %d", inviteEvent.Kind, nip43.KindInviteReq)
}
// Verify claim tag
claimTag := inviteEvent.Tags.GetFirst([]byte("claim"))
if claimTag == nil {
t.Fatal("missing claim tag")
}
if claimTag.Len() < 2 {
t.Fatal("claim tag has no value")
}
}
// TestHandleNIP43InviteRequest_Unauthorized tests invite request from non-admin
func TestHandleNIP43InviteRequest_Unauthorized(t *testing.T) {
listener, _, cleanup := setupTestListener(t)
defer cleanup()
// Generate regular user (not admin)
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Handle invite request - should fail
_, err = listener.Server.HandleNIP43InviteRequest(userPubkey)
if err == nil {
t.Fatal("expected error for unauthorized user")
}
}
// TestJoinAndLeaveFlow tests the complete join and leave flow
func TestJoinAndLeaveFlow(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
// Generate test user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Fatalf("failed to generate user secret: %v", err)
}
userSigner, err := p8k.New()
if err != nil {
t.Fatalf("failed to create signer: %v", err)
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Fatalf("failed to initialize signer: %v", err)
}
userPubkey := userSigner.Pub()
// Step 1: Generate invite code
code, err := listener.Server.InviteManager.GenerateCode()
if err != nil {
t.Fatalf("failed to generate invite code: %v", err)
}
// Step 2: User sends join request
joinEv := event.New()
joinEv.Kind = nip43.KindJoinRequest
copy(joinEv.Pubkey, userPubkey)
joinEv.Tags = tag.NewS()
joinEv.Tags.Append(tag.NewFromAny("-"))
joinEv.Tags.Append(tag.NewFromAny("claim", code))
joinEv.CreatedAt = time.Now().Unix()
joinEv.Content = []byte("")
if err = joinEv.Sign(userSigner); err != nil {
t.Fatalf("failed to sign join event: %v", err)
}
err = listener.HandleNIP43JoinRequest(joinEv)
if err != nil {
t.Fatalf("failed to handle join request: %v", err)
}
// Verify user is member
isMember, err := db.IsNIP43Member(userPubkey)
if err != nil {
t.Fatalf("failed to check membership after join: %v", err)
}
if !isMember {
t.Fatal("user is not a member after join")
}
// Step 3: User sends leave request
leaveEv := event.New()
leaveEv.Kind = nip43.KindLeaveRequest
copy(leaveEv.Pubkey, userPubkey)
leaveEv.Tags = tag.NewS()
leaveEv.Tags.Append(tag.NewFromAny("-"))
leaveEv.CreatedAt = time.Now().Unix()
leaveEv.Content = []byte("")
if err = leaveEv.Sign(userSigner); err != nil {
t.Fatalf("failed to sign leave event: %v", err)
}
err = listener.HandleNIP43LeaveRequest(leaveEv)
if err != nil {
t.Fatalf("failed to handle leave request: %v", err)
}
// Verify user is no longer member
isMember, err = db.IsNIP43Member(userPubkey)
if err != nil {
t.Fatalf("failed to check membership after leave: %v", err)
}
if isMember {
t.Fatal("user is still a member after leave")
}
}
// TestMultipleUsersJoining tests multiple users joining concurrently
func TestMultipleUsersJoining(t *testing.T) {
listener, db, cleanup := setupTestListener(t)
defer cleanup()
userCount := 10
done := make(chan bool, userCount)
for i := 0; i < userCount; i++ {
go func(index int) {
// Generate user
userSecret, err := keys.GenerateSecretKey()
if err != nil {
t.Errorf("failed to generate user secret %d: %v", index, err)
done <- false
return
}
userSigner, err := p8k.New()
if err != nil {
t.Errorf("failed to create signer %d: %v", index, err)
done <- false
return
}
if err = userSigner.InitSec(userSecret); err != nil {
t.Errorf("failed to initialize signer %d: %v", index, err)
done <- false
return
}
userPubkey := userSigner.Pub()
// Generate invite code
code, err := listener.Server.InviteManager.GenerateCode()
if err != nil {
t.Errorf("failed to generate invite code %d: %v", index, err)
done <- false
return
}
// Create join request
joinEv := event.New()
joinEv.Kind = nip43.KindJoinRequest
copy(joinEv.Pubkey, userPubkey)
joinEv.Tags = tag.NewS()
joinEv.Tags.Append(tag.NewFromAny("-"))
joinEv.Tags.Append(tag.NewFromAny("claim", code))
joinEv.CreatedAt = time.Now().Unix()
joinEv.Content = []byte("")
if err = joinEv.Sign(userSigner); err != nil {
t.Errorf("failed to sign event %d: %v", index, err)
done <- false
return
}
// Handle join request
if err = listener.HandleNIP43JoinRequest(joinEv); err != nil {
t.Errorf("failed to handle join request %d: %v", index, err)
done <- false
return
}
done <- true
}(i)
}
// Wait for all goroutines
successCount := 0
for i := 0; i < userCount; i++ {
if <-done {
successCount++
}
}
if successCount != userCount {
t.Errorf("not all users joined successfully: %d/%d", successCount, userCount)
}
// Verify member count
members, err := db.GetAllNIP43Members()
if err != nil {
t.Fatalf("failed to get all members: %v", err)
}
if len(members) != successCount {
t.Errorf("wrong member count: got %d, want %d", len(members), successCount)
}
}