571 lines
15 KiB
Go
571 lines
15 KiB
Go
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)
|
|
}
|
|
}
|