返回 DeepSeek-Reasonix
weixin_test.go
根目录 / internal / bot / weixin / weixin_test.go
1 package weixin
2
3 import (
4 "context"
5 "encoding/json"
6 "io"
7 "log/slog"
8 "net/http"
9 "net/http/httptest"
10 "path/filepath"
11 "strings"
12 "testing"
13 "time"
14
15 "reasonix/internal/bot"
16 "reasonix/internal/config"
17 )
18
19 func TestStartReturnsMissingToken(t *testing.T) {
20 isolateWeixinUserConfig(t)
21 t.Setenv("WEIXIN_TEST_TOKEN", "")
22 a := New(config.WeixinBotConfig{
23 TokenEnv: "WEIXIN_TEST_TOKEN",
24 AccountID: "missing-account",
25 }, slog.New(slog.NewTextHandler(io.Discard, nil)))
26
27 err := a.Start(context.Background())
28 if err == nil || !strings.Contains(err.Error(), "WEIXIN_TEST_TOKEN") {
29 t.Fatalf("Start error = %v, want missing token env", err)
30 }
31 }
32
33 func TestSendTextPostsIlinkMessage(t *testing.T) {
34 t.Setenv("WEIXIN_TEST_TOKEN", "token-1")
35 var gotAuth string
36 var gotPayload map[string]any
37 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
38 if r.URL.Path != sendMessagePath {
39 http.NotFound(w, r)
40 return
41 }
42 gotAuth = r.Header.Get("Authorization")
43 if err := json.NewDecoder(r.Body).Decode(&gotPayload); err != nil {
44 t.Fatalf("decode request: %v", err)
45 }
46 _ = json.NewEncoder(w).Encode(map[string]any{
47 "ret": 0,
48 "errcode": 0,
49 "message_id": "wx-msg-1",
50 })
51 }))
52 defer server.Close()
53
54 result, err := SendText(context.Background(), config.WeixinBotConfig{
55 TokenEnv: "WEIXIN_TEST_TOKEN",
56 APIBase: server.URL,
57 }, "chat-1", "hello weixin")
58 if err != nil {
59 t.Fatalf("SendText: %v", err)
60 }
61 if result.MessageID != "wx-msg-1" {
62 t.Fatalf("message id = %q, want wx-msg-1", result.MessageID)
63 }
64 if gotAuth != "Bearer token-1" {
65 t.Fatalf("Authorization = %q, want Bearer token-1", gotAuth)
66 }
67 msg, ok := gotPayload["msg"].(map[string]any)
68 if !ok {
69 t.Fatalf("payload msg = %#v, want object", gotPayload["msg"])
70 }
71 if msg["to_user_id"] != "chat-1" || msg["message_type"] != float64(weixinMsgTypeBot) || msg["message_state"] != float64(weixinMsgStateDone) {
72 t.Fatalf("msg metadata = %#v", msg)
73 }
74 items, ok := msg["item_list"].([]any)
75 if !ok || len(items) != 1 {
76 t.Fatalf("item_list = %#v, want one text item", msg["item_list"])
77 }
78 item, ok := items[0].(map[string]any)
79 if !ok || item["type"] != float64(weixinItemText) {
80 t.Fatalf("item = %#v, want text item", items[0])
81 }
82 textItem, ok := item["text_item"].(map[string]any)
83 if !ok || textItem["text"] != "hello weixin" {
84 t.Fatalf("text item = %#v, want hello weixin", item["text_item"])
85 }
86 }
87
88 func isolateWeixinUserConfig(t *testing.T) {
89 t.Helper()
90 home := t.TempDir()
91 t.Setenv("HOME", home)
92 t.Setenv("USERPROFILE", home)
93 t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
94 t.Setenv("AppData", filepath.Join(home, "AppData"))
95 }
96
97 func TestLogPollHealthThrottlesEmptyPolls(t *testing.T) {
98 a := &adapter{logger: slog.Default().With("platform", "weixin")}
99 a.logPollHealth(ilinkResponse{})
100 first := a.lastPollLog
101 if first.IsZero() {
102 t.Fatal("first empty poll should update heartbeat timestamp")
103 }
104 a.logPollHealth(ilinkResponse{})
105 if !a.lastPollLog.Equal(first) {
106 t.Fatalf("second empty poll updated heartbeat timestamp: got %v want %v", a.lastPollLog, first)
107 }
108 stale := time.Now().Add(-6 * time.Minute)
109 a.lastPollLog = stale
110 a.logPollHealth(ilinkResponse{})
111 if !a.lastPollLog.After(stale) {
112 t.Fatalf("stale empty poll did not refresh heartbeat timestamp: got %v after %v", a.lastPollLog, stale)
113 }
114 }
115
116 func TestLogPollHealthLogsNonEmptyPolls(t *testing.T) {
117 a := &adapter{logger: slog.Default().With("platform", "weixin")}
118 a.logPollHealth(ilinkResponse{Msgs: []ilinkMessage{{MessageID: "msg-1"}}})
119 if a.lastPollLog.IsZero() {
120 t.Fatal("non-empty poll should update heartbeat timestamp")
121 }
122 }
123
124 func TestGetUpdatesAcceptsNumericIlinkMessageID(t *testing.T) {
125 t.Setenv("WEIXIN_TEST_TOKEN", "token-1")
126 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127 if r.URL.Path != getUpdatesPath {
128 http.NotFound(w, r)
129 return
130 }
131 _ = json.NewEncoder(w).Encode(map[string]any{
132 "ret": 0,
133 "errcode": 0,
134 "msgs": []map[string]any{
135 {
136 "message_id": 123456789,
137 "from_user_id": "wx-user-1",
138 "to_user_id": "bot-account",
139 "msg_type": 1,
140 "item_list": []map[string]any{
141 {"type": weixinItemText, "text_item": map[string]string{"text": "hello"}},
142 },
143 },
144 },
145 })
146 }))
147 defer server.Close()
148
149 a := &adapter{
150 cfg: config.WeixinBotConfig{
151 TokenEnv: "WEIXIN_TEST_TOKEN",
152 APIBase: server.URL,
153 AccountID: "bot-account",
154 },
155 logger: slog.Default().With("platform", "weixin"),
156 msgCh: make(chan bot.InboundMessage, 1),
157 contextTokens: make(map[string]string),
158 }
159 updates, err := a.getUpdates(context.Background())
160 if err != nil {
161 t.Fatalf("getUpdates: %v", err)
162 }
163 if len(updates) != 0 {
164 t.Fatalf("updates = %d, want 0", len(updates))
165 }
166 select {
167 case msg := <-a.msgCh:
168 if msg.MessageID != "123456789" || msg.UserID != "wx-user-1" || msg.Text != "hello" {
169 t.Fatalf("message = %+v, want numeric id converted and text preserved", msg)
170 }
171 case <-context.Background().Done():
172 t.Fatal("unreachable")
173 default:
174 t.Fatal("expected queued inbound message")
175 }
176 }
177
177 lines GO