1package telegram
2
3import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "errors"
8 "io"
9 "net/http"
10 "strings"
11 "sync"
12 "testing"
13 "time"
14
15 "github.com/patriceckhart/zot/packages/agent/modes/bot"
16 "github.com/patriceckhart/zot/packages/provider"
17)
18
19type roundTripFunc func(*http.Request) (*http.Response, error)
20
21func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
22 return f(req)
23}
24
25func telegramResponse(body string) *http.Response {
26 return &http.Response{
27 StatusCode: http.StatusOK,
28 Body: io.NopCloser(strings.NewReader(body)),
29 Header: make(http.Header),
30 }
31}
32
33func testClient(transport http.RoundTripper) *Client {
34 return &Client{
35 token: "test",
36 http: &http.Client{Transport: transport},
37 }
38}
39
40func TestClientDeleteWebhookPreservesPendingUpdates(t *testing.T) {
41 client := testClient(roundTripFunc(func(req *http.Request) (*http.Response, error) {
42 if req.Method != http.MethodPost {
43 t.Fatalf("method = %s, want POST", req.Method)
44 }
45 if req.URL.Path != "/bottest/deleteWebhook" {
46 t.Fatalf("path = %q, want deleteWebhook endpoint", req.URL.Path)
47 }
48 body, err := io.ReadAll(req.Body)
49 if err != nil {
50 t.Fatal(err)
51 }
52 var got map[string]bool
53 if err := json.Unmarshal(body, &got); err != nil {
54 t.Fatal(err)
55 }
56 if got["drop_pending_updates"] {
57 t.Fatal("drop_pending_updates = true, want false")
58 }
59 return telegramResponse(`{"ok":true,"result":true}`), nil
60 }))
61
62 if err := client.DeleteWebhook(context.Background(), false); err != nil {
63 t.Fatalf("DeleteWebhook() error = %v", err)
64 }
65}
66
67func TestClientDeleteWebhookRejectsAPIFailure(t *testing.T) {
68 client := testClient(roundTripFunc(func(*http.Request) (*http.Response, error) {
69 return telegramResponse(`{"ok":false,"description":"not allowed","result":false}`), nil
70 }))
71
72 err := client.DeleteWebhook(context.Background(), false)
73 if err == nil || !strings.Contains(err.Error(), "deleteWebhook: not allowed") {
74 t.Fatalf("DeleteWebhook() error = %v, want Telegram API error", err)
75 }
76}
77
78func TestAdapterRunDeletesWebhookBeforePolling(t *testing.T) {
79 ctx, cancel := context.WithCancel(context.Background())
80 defer cancel()
81
82 var (
83 mu sync.Mutex
84 calls []string
85 )
86 client := testClient(roundTripFunc(func(req *http.Request) (*http.Response, error) {
87 endpoint := req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]
88 mu.Lock()
89 calls = append(calls, endpoint)
90 mu.Unlock()
91 switch endpoint {
92 case "getMe":
93 return telegramResponse(`{"ok":true,"result":{"id":1,"is_bot":true,"username":"zot_test"}}`), nil
94 case "deleteWebhook":
95 return telegramResponse(`{"ok":true,"result":true}`), nil
96 case "getUpdates":
97 cancel()
98 return telegramResponse(`{"ok":true,"result":[]}`), nil
99 default:
100 t.Fatalf("unexpected endpoint %q", endpoint)
101 return nil, errors.New("unreachable")
102 }
103 }))
104 cfg := Config{BotToken: "test", BotID: 1, BotUsername: "zot_test"}
105 adapter := NewAdapter(client, &cfg, func(Config) error { return nil })
106
107 err := adapter.Run(ctx, func(bot.InboundMessage) {}, func(bot.Command, bot.InboundMessage) {})
108 if !errors.Is(err, context.Canceled) {
109 t.Fatalf("Run() error = %v, want context canceled", err)
110 }
111 mu.Lock()
112 defer mu.Unlock()
113 want := []string{"getMe", "deleteWebhook", "getUpdates"}
114 if strings.Join(calls, ",") != strings.Join(want, ",") {
115 t.Fatalf("API calls = %v, want %v", calls, want)
116 }
117}
118
119func TestBridgeStartDeletesWebhookBeforePolling(t *testing.T) {
120 pollStarted := make(chan struct{})
121 var pollOnce sync.Once
122 var (
123 mu sync.Mutex
124 calls []string
125 )
126 client := testClient(roundTripFunc(func(req *http.Request) (*http.Response, error) {
127 endpoint := req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]
128 mu.Lock()
129 calls = append(calls, endpoint)
130 mu.Unlock()
131 switch endpoint {
132 case "getMe":
133 return telegramResponse(`{"ok":true,"result":{"id":1,"is_bot":true,"username":"zot_test"}}`), nil
134 case "deleteWebhook":
135 return telegramResponse(`{"ok":true,"result":true}`), nil
136 case "getUpdates":
137 pollOnce.Do(func() { close(pollStarted) })
138 <-req.Context().Done()
139 return nil, req.Context().Err()
140 default:
141 t.Fatalf("unexpected endpoint %q", endpoint)
142 return nil, errors.New("unreachable")
143 }
144 }))
145 bridge := &Bridge{
146 Client: client,
147 Config: Config{BotToken: "test", BotID: 1, BotUsername: "zot_test"},
148 Save: func(Config) error { return nil },
149 Host: testBridgeHost{},
150 }
151
152 if err := bridge.Start(context.Background()); err != nil {
153 t.Fatalf("Start() error = %v", err)
154 }
155 select {
156 case <-pollStarted:
157 case <-time.After(2 * time.Second):
158 t.Fatal("timed out waiting for Telegram polling to start")
159 }
160 bridge.Stop()
161
162 mu.Lock()
163 defer mu.Unlock()
164 want := []string{"getMe", "deleteWebhook", "getUpdates"}
165 if strings.Join(calls, ",") != strings.Join(want, ",") {
166 t.Fatalf("API calls = %v, want %v", calls, want)
167 }
168}
169
170type testBridgeHost struct{}
171
172func (testBridgeHost) SubmitOrQueue(string, []provider.ImageBlock) {}
173func (testBridgeHost) CancelTurn() {}
174func (testBridgeHost) Status() string { return "" }
175func (testBridgeHost) Notify(string, string) {}
176
177func TestAdapterRunFailsBeforePollingWhenWebhookRemovalFails(t *testing.T) {
178 var calls bytes.Buffer
179 client := testClient(roundTripFunc(func(req *http.Request) (*http.Response, error) {
180 endpoint := req.URL.Path[strings.LastIndex(req.URL.Path, "/")+1:]
181 calls.WriteString(endpoint + "\n")
182 if endpoint == "getMe" {
183 return telegramResponse(`{"ok":true,"result":{"id":1,"is_bot":true,"username":"zot_test"}}`), nil
184 }
185 return telegramResponse(`{"ok":false,"description":"webhook removal denied","result":false}`), nil
186 }))
187 cfg := Config{BotToken: "test"}
188 adapter := NewAdapter(client, &cfg, func(Config) error { return nil })
189
190 err := adapter.Run(context.Background(), func(bot.InboundMessage) {}, func(bot.Command, bot.InboundMessage) {})
191 if err == nil || !strings.Contains(err.Error(), "remove telegram webhook before polling") {
192 t.Fatalf("Run() error = %v, want actionable webhook error", err)
193 }
194 if got, want := calls.String(), "getMe\ndeleteWebhook\n"; got != want {
195 t.Fatalf("API calls = %q, want %q", got, want)
196 }
197}