You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
600 lines
16 KiB
600 lines
16 KiB
package app |
|
|
|
import ( |
|
"context" |
|
"os" |
|
"testing" |
|
"time" |
|
|
|
"next.orly.dev/app/config" |
|
"next.orly.dev/pkg/acl" |
|
"git.mleku.dev/mleku/nostr/crypto/keys" |
|
"next.orly.dev/pkg/database" |
|
"git.mleku.dev/mleku/nostr/encoders/event" |
|
"git.mleku.dev/mleku/nostr/encoders/hex" |
|
"git.mleku.dev/mleku/nostr/encoders/tag" |
|
"git.mleku.dev/mleku/nostr/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, |
|
ACLMode: "none", |
|
} |
|
|
|
server := &Server{ |
|
Ctx: ctx, |
|
Config: cfg, |
|
DB: db, |
|
publishers: publish.New(NewPublisher(ctx)), |
|
InviteManager: nip43.NewInviteManager(cfg.NIP43InviteExpiry), |
|
cfg: cfg, |
|
db: db, |
|
} |
|
|
|
// Configure ACL registry |
|
acl.Registry.SetMode(cfg.ACLMode) |
|
if err = acl.Registry.Configure(cfg, db, ctx); err != nil { |
|
db.Close() |
|
os.RemoveAll(tempDir) |
|
t.Fatalf("failed to configure ACL: %v", err) |
|
} |
|
|
|
listener := &Listener{ |
|
Server: server, |
|
ctx: ctx, |
|
writeChan: make(chan publish.WriteRequest, 100), |
|
writeDone: make(chan struct{}), |
|
messageQueue: make(chan messageRequest, 100), |
|
processingDone: make(chan struct{}), |
|
subscriptions: make(map[string]context.CancelFunc), |
|
} |
|
|
|
// Start write worker and message processor |
|
go listener.writeWorker() |
|
go listener.messageProcessor() |
|
|
|
cleanup := func() { |
|
// Close listener channels |
|
close(listener.writeChan) |
|
<-listener.writeDone |
|
close(listener.messageQueue) |
|
<-listener.processingDone |
|
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 config and reconfigure ACL |
|
adminHex := hex.Enc(adminPubkey) |
|
listener.Server.Config.Admins = []string{adminHex} |
|
acl.Registry.SetMode("none") |
|
if err = acl.Registry.Configure(listener.Server.Config, listener.Server.DB, listener.ctx); err != nil { |
|
t.Fatalf("failed to reconfigure ACL: %v", err) |
|
} |
|
|
|
// 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) |
|
} |
|
}
|
|
|