v0.2.94 7e6033a

api_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 477

↑↓ moveenter openesc close