router_test.go raw
1 package bridge
2
3 import (
4 "context"
5 "fmt"
6 "strings"
7 "sync"
8 "testing"
9 "time"
10 )
11
12 type mockDMSink struct {
13 mu sync.Mutex
14 msgs map[string][]string
15 }
16
17 func newMockDMSink() *mockDMSink {
18 return &mockDMSink{msgs: make(map[string][]string)}
19 }
20
21 func (m *mockDMSink) send(pubkey, content string) error {
22 m.mu.Lock()
23 defer m.mu.Unlock()
24 m.msgs[pubkey] = append(m.msgs[pubkey], content)
25 return nil
26 }
27
28 func (m *mockDMSink) get(pubkey string) []string {
29 m.mu.Lock()
30 defer m.mu.Unlock()
31 return m.msgs[pubkey]
32 }
33
34 func TestRouter_Subscribe(t *testing.T) {
35 sink := newMockDMSink()
36
37 // No subscription handler — should get "not configured" response
38 router := NewRouter(nil, nil, sink.send)
39 router.RouteDM(context.Background(), "user1", "subscribe")
40
41 msgs := sink.get("user1")
42 if len(msgs) != 1 {
43 t.Fatalf("expected 1 message, got %d", len(msgs))
44 }
45 if !strings.Contains(msgs[0], "not configured") {
46 t.Errorf("expected 'not configured' message, got: %s", msgs[0])
47 }
48 }
49
50 func TestRouter_OutboundEmail(t *testing.T) {
51 sink := newMockDMSink()
52
53 // No outbound processor — should get "not configured" response
54 router := NewRouter(nil, nil, sink.send)
55 router.RouteDM(context.Background(), "user1", "To: alice@example.com\nSubject: Test\n\nHello")
56
57 msgs := sink.get("user1")
58 if len(msgs) != 1 {
59 t.Fatalf("expected 1 message, got %d", len(msgs))
60 }
61 if !strings.Contains(msgs[0], "not configured") {
62 t.Errorf("expected 'not configured' message, got: %s", msgs[0])
63 }
64 }
65
66 func TestRouter_UnrecognizedMessage(t *testing.T) {
67 sink := newMockDMSink()
68
69 router := NewRouter(nil, nil, sink.send)
70 router.RouteDM(context.Background(), "user1", "just a regular hello message")
71
72 msgs := sink.get("user1")
73 if len(msgs) != 1 {
74 t.Fatalf("expected 1 help message, got %d", len(msgs))
75 }
76 if !strings.Contains(msgs[0], "Marmot Email Bridge") {
77 t.Errorf("expected help text, got: %s", msgs[0])
78 }
79 if !strings.Contains(msgs[0], "subscribe") {
80 t.Error("help text should mention subscribe command")
81 }
82 }
83
84 func TestRouter_WithSubscriptionHandler(t *testing.T) {
85 sink := newMockDMSink()
86 store := NewMemorySubscriptionStore()
87
88 // Create handler with nil payment processor — will get "not available" reply
89 subHandler := NewSubscriptionHandler(store, nil, sink.send, 2100, nil, 0)
90 router := NewRouter(subHandler, nil, sink.send)
91
92 router.RouteDM(context.Background(), "user1", "subscribe")
93
94 // HandleSubscribe runs in a goroutine, wait briefly for it to complete
95 time.Sleep(50 * time.Millisecond)
96
97 // Should have been routed to the subscription handler,
98 // which sends a "not available" message because payments=nil
99 msgs := sink.get("user1")
100 if len(msgs) != 1 {
101 t.Fatalf("expected 1 message, got %d", len(msgs))
102 }
103 if !strings.Contains(msgs[0], "not available") {
104 t.Errorf("expected 'not available' from sub handler, got: %s", msgs[0])
105 }
106 }
107
108 func TestRouter_OutboundWithProcessor(t *testing.T) {
109 sink := newMockDMSink()
110
111 // Create outbound processor with nil SMTP client — will fail at send time
112 outbound := NewOutboundProcessor(nil, nil, nil, "test.example.com", sink.send, nil)
113 router := NewRouter(nil, outbound, sink.send)
114
115 // This will hit the SMTP send which panics because smtpClient is nil.
116 defer func() {
117 if r := recover(); r != nil {
118 t.Logf("expected panic from nil SMTP client: %v", r)
119 }
120 }()
121
122 router.RouteDM(context.Background(), "user1", "To: alice@example.com\nSubject: Test\n\nBody")
123 }
124
125 func TestRouter_Reply_NilSendDM(t *testing.T) {
126 router := NewRouter(nil, nil, nil)
127 // Should not panic
128 router.reply("user1", "test")
129 }
130
131 func TestRouter_Reply_Error(t *testing.T) {
132 errorSend := func(pubkey, content string) error {
133 return fmt.Errorf("send failed")
134 }
135 router := NewRouter(nil, nil, errorSend)
136 // Should not panic, just log
137 router.reply("user1", "test")
138 }
139
140 func TestGenerateReplyLink(t *testing.T) {
141 link := GenerateReplyLink("https://bridge.example.com/compose", "alice@example.com", "Hello")
142 if !strings.Contains(link, "bridge.example.com/compose") {
143 t.Errorf("link missing base URL: %s", link)
144 }
145 if !strings.Contains(link, "#to=alice%40example.com") {
146 t.Errorf("link missing to param: %s", link)
147 }
148 if !strings.Contains(link, "subject=Re%3A+Hello") {
149 t.Errorf("link missing subject: %s", link)
150 }
151 }
152
153 func TestGenerateReplyLink_AlreadyRe(t *testing.T) {
154 link := GenerateReplyLink("https://example.com/compose", "bob@test.com", "Re: Original")
155 if strings.Contains(link, "Re: Re:") {
156 t.Errorf("double Re: in link: %s", link)
157 }
158 }
159