subscription_handler_test.go raw

   1  package bridge
   2  
   3  import (
   4  	"context"
   5  	"fmt"
   6  	"strings"
   7  	"sync"
   8  	"testing"
   9  	"time"
  10  )
  11  
  12  // testDMCollector records DMs sent by the subscription handler.
  13  type testDMCollector struct {
  14  	mu       sync.Mutex
  15  	messages map[string][]string
  16  }
  17  
  18  func newTestDMCollector() *testDMCollector {
  19  	return &testDMCollector{messages: make(map[string][]string)}
  20  }
  21  
  22  func (c *testDMCollector) sendDM(pubkeyHex, content string) error {
  23  	c.mu.Lock()
  24  	defer c.mu.Unlock()
  25  	c.messages[pubkeyHex] = append(c.messages[pubkeyHex], content)
  26  	return nil
  27  }
  28  
  29  func (c *testDMCollector) get(pubkeyHex string) []string {
  30  	c.mu.Lock()
  31  	defer c.mu.Unlock()
  32  	return c.messages[pubkeyHex]
  33  }
  34  
  35  func TestSubscriptionHandler_IsSubscribed(t *testing.T) {
  36  	store := NewMemorySubscriptionStore()
  37  
  38  	sh := NewSubscriptionHandler(store, nil, nil, 2100, nil, 0)
  39  
  40  	if sh.IsSubscribed("abc123") {
  41  		t.Error("should not be subscribed before saving")
  42  	}
  43  
  44  	store.Save(&Subscription{
  45  		PubkeyHex: "abc123",
  46  		ExpiresAt: time.Now().Add(24 * time.Hour),
  47  		CreatedAt: time.Now(),
  48  	})
  49  
  50  	if !sh.IsSubscribed("abc123") {
  51  		t.Error("should be subscribed after saving")
  52  	}
  53  
  54  	// Expired subscription
  55  	store.Save(&Subscription{
  56  		PubkeyHex: "expired",
  57  		ExpiresAt: time.Now().Add(-1 * time.Hour),
  58  		CreatedAt: time.Now().Add(-25 * time.Hour),
  59  	})
  60  
  61  	if sh.IsSubscribed("expired") {
  62  		t.Error("expired subscription should not be active")
  63  	}
  64  }
  65  
  66  func TestSubscriptionHandler_HandleSubscribe_AlreadyActive(t *testing.T) {
  67  	store := NewMemorySubscriptionStore()
  68  	store.Save(&Subscription{
  69  		PubkeyHex: "abc123",
  70  		ExpiresAt: time.Now().Add(15 * 24 * time.Hour),
  71  		CreatedAt: time.Now().Add(-15 * 24 * time.Hour),
  72  	})
  73  
  74  	dms := newTestDMCollector()
  75  
  76  	// payments=nil is fine because we shouldn't reach the payment code
  77  	sh := NewSubscriptionHandler(store, nil, dms.sendDM, 2100, nil, 0)
  78  
  79  	ctx := context.Background()
  80  	sh.HandleSubscribe(ctx, "abc123", "")
  81  
  82  	msgs := dms.get("abc123")
  83  	if len(msgs) != 1 {
  84  		t.Fatalf("expected 1 DM, got %d", len(msgs))
  85  	}
  86  	if got := msgs[0]; len(got) == 0 {
  87  		t.Error("expected non-empty already-active message")
  88  	}
  89  }
  90  
  91  func TestSubscriptionHandler_HandleSubscribe_NoPaymentProcessor(t *testing.T) {
  92  	store := NewMemorySubscriptionStore()
  93  	dms := newTestDMCollector()
  94  
  95  	sh := NewSubscriptionHandler(store, nil, dms.sendDM, 2100, nil, 0)
  96  
  97  	defer func() {
  98  		if r := recover(); r != nil {
  99  			t.Errorf("HandleSubscribe panicked: %v", r)
 100  		}
 101  	}()
 102  
 103  	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 104  	defer cancel()
 105  
 106  	sh.HandleSubscribe(ctx, "newuser", "")
 107  
 108  	msgs := dms.get("newuser")
 109  	if len(msgs) != 1 {
 110  		t.Fatalf("expected 1 DM, got %d", len(msgs))
 111  	}
 112  	if !strings.Contains(msgs[0], "not available") {
 113  		t.Errorf("expected 'not available' message, got: %s", msgs[0])
 114  	}
 115  }
 116  
 117  func TestSubscriptionHandler_HandleSubscribe_InvoiceCreationFails(t *testing.T) {
 118  	store := NewMemorySubscriptionStore()
 119  	dms := newTestDMCollector()
 120  
 121  	mock := newMockNWC()
 122  	mock.errors["make_invoice"] = fmt.Errorf("wallet offline")
 123  	pp := NewPaymentProcessorWithClient(mock, 2100)
 124  
 125  	sh := NewSubscriptionHandler(store, pp, dms.sendDM, 2100, nil, 0)
 126  
 127  	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
 128  	defer cancel()
 129  
 130  	sh.HandleSubscribe(ctx, "user1", "")
 131  
 132  	msgs := dms.get("user1")
 133  	if len(msgs) != 1 {
 134  		t.Fatalf("expected 1 DM, got %d", len(msgs))
 135  	}
 136  	if !strings.Contains(msgs[0], "Failed to create invoice") {
 137  		t.Errorf("expected invoice failure message, got: %s", msgs[0])
 138  	}
 139  }
 140  
 141  func TestSubscriptionHandler_HandleSubscribe_FullFlow(t *testing.T) {
 142  	store := NewMemorySubscriptionStore()
 143  	dms := newTestDMCollector()
 144  
 145  	mock := newMockNWC()
 146  	mock.responses["make_invoice"] = map[string]any{
 147  		"invoice":      "lnbc21000n1...",
 148  		"payment_hash": "abc123",
 149  		"amount":       2100000,
 150  	}
 151  	mock.responses["lookup_invoice"] = map[string]any{
 152  		"payment_hash": "abc123",
 153  		"settled_at":   1700000000,
 154  		"preimage":     "deadbeef",
 155  	}
 156  	pp := NewPaymentProcessorWithClient(mock, 2100)
 157  
 158  	sh := NewSubscriptionHandler(store, pp, dms.sendDM, 2100, nil, 0)
 159  
 160  	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 161  	defer cancel()
 162  
 163  	sh.HandleSubscribe(ctx, "user1", "")
 164  
 165  	msgs := dms.get("user1")
 166  	if len(msgs) < 2 {
 167  		t.Fatalf("expected at least 2 DMs (invoice + confirmation), got %d", len(msgs))
 168  	}
 169  	// First message: invoice
 170  	if !strings.Contains(msgs[0], "lnbc21000n1") {
 171  		t.Errorf("first DM should contain invoice, got: %s", msgs[0])
 172  	}
 173  	// Last message: confirmation
 174  	last := msgs[len(msgs)-1]
 175  	if !strings.Contains(last, "Payment received") {
 176  		t.Errorf("last DM should confirm payment, got: %s", last)
 177  	}
 178  
 179  	// Verify subscription was saved
 180  	if !sh.IsSubscribed("user1") {
 181  		t.Error("user1 should be subscribed after payment")
 182  	}
 183  }
 184  
 185  func TestSubscriptionHandler_HandleSubscribe_PaymentTimeout(t *testing.T) {
 186  	store := NewMemorySubscriptionStore()
 187  	dms := newTestDMCollector()
 188  
 189  	mock := newMockNWC()
 190  	mock.responses["make_invoice"] = map[string]any{
 191  		"invoice":      "lnbc21000n1...",
 192  		"payment_hash": "abc123",
 193  		"amount":       2100000,
 194  	}
 195  	// lookup_invoice always returns unpaid
 196  	mock.responses["lookup_invoice"] = map[string]any{
 197  		"payment_hash": "abc123",
 198  	}
 199  	pp := NewPaymentProcessorWithClient(mock, 2100)
 200  
 201  	sh := NewSubscriptionHandler(store, pp, dms.sendDM, 2100, nil, 0)
 202  
 203  	// Short timeout so the test doesn't take forever
 204  	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
 205  	defer cancel()
 206  
 207  	sh.HandleSubscribe(ctx, "user1", "")
 208  
 209  	// Should NOT be subscribed — payment timed out
 210  	if sh.IsSubscribed("user1") {
 211  		t.Error("user1 should not be subscribed after timeout")
 212  	}
 213  }
 214  
 215  func TestSubscriptionHandler_HandleSubscribe_SaveFails(t *testing.T) {
 216  	dms := newTestDMCollector()
 217  
 218  	mock := newMockNWC()
 219  	mock.responses["make_invoice"] = map[string]any{
 220  		"invoice":      "lnbc21000n1...",
 221  		"payment_hash": "abc123",
 222  	}
 223  	mock.responses["lookup_invoice"] = map[string]any{
 224  		"payment_hash": "abc123",
 225  		"settled_at":   1700000000,
 226  	}
 227  	pp := NewPaymentProcessorWithClient(mock, 2100)
 228  
 229  	// Use a store that fails on Save
 230  	failStore := &failingSaveStore{}
 231  
 232  	sh := NewSubscriptionHandler(failStore, pp, dms.sendDM, 2100, nil, 0)
 233  
 234  	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
 235  	defer cancel()
 236  
 237  	sh.HandleSubscribe(ctx, "user1", "")
 238  
 239  	msgs := dms.get("user1")
 240  	// Should get invoice message + failure message
 241  	found := false
 242  	for _, m := range msgs {
 243  		if strings.Contains(m, "failed to activate") {
 244  			found = true
 245  		}
 246  	}
 247  	if !found {
 248  		t.Errorf("expected save failure message, got: %v", msgs)
 249  	}
 250  }
 251  
 252  func TestSubscriptionHandler_SendReply_Error(t *testing.T) {
 253  	store := NewMemorySubscriptionStore()
 254  	sh := NewSubscriptionHandler(store, nil, func(pk, c string) error {
 255  		return fmt.Errorf("send error")
 256  	}, 2100, nil, 0)
 257  	// Should not panic, just log
 258  	sh.sendReply("user1", "test")
 259  }
 260  
 261  // failingSaveStore is a SubscriptionStore that always fails on Save.
 262  type failingSaveStore struct {
 263  	MemorySubscriptionStore
 264  }
 265  
 266  func (f *failingSaveStore) Save(sub *Subscription) error {
 267  	return fmt.Errorf("save failed")
 268  }
 269  
 270  func (f *failingSaveStore) Get(pubkeyHex string) (*Subscription, error) {
 271  	return nil, fmt.Errorf("not found")
 272  }
 273  
 274  func (f *failingSaveStore) List() ([]*Subscription, error) {
 275  	return nil, nil
 276  }
 277  
 278  func (f *failingSaveStore) Delete(pubkeyHex string) error {
 279  	return nil
 280  }
 281