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