返回 DeepSeek-Reasonix
bot_connection_app_test.go
根目录 / desktop / bot_connection_app_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "net/url"
8 "os"
9 "strings"
10 "testing"
11
12 "reasonix/internal/config"
13 )
14
15 func TestNormalizeBotInstallTarget(t *testing.T) {
16 cases := []struct {
17 provider string
18 domain string
19 wantProvider string
20 wantDomain string
21 }{
22 {provider: "lark", wantProvider: "feishu", wantDomain: "lark"},
23 {provider: "feishu", domain: "lark", wantProvider: "feishu", wantDomain: "lark"},
24 {provider: "wechat", wantProvider: "weixin", wantDomain: "weixin"},
25 {provider: "weixin", domain: "anything", wantProvider: "weixin", wantDomain: "weixin"},
26 {provider: "unknown", domain: "unknown", wantProvider: "feishu", wantDomain: "feishu"},
27 }
28 for _, tc := range cases {
29 gotProvider, gotDomain := normalizeBotInstallTarget(tc.provider, tc.domain)
30 if gotProvider != tc.wantProvider || gotDomain != tc.wantDomain {
31 t.Fatalf("normalizeBotInstallTarget(%q,%q) = %q,%q; want %q,%q", tc.provider, tc.domain, gotProvider, gotDomain, tc.wantProvider, tc.wantDomain)
32 }
33 }
34 }
35
36 func TestLarkInstallFollowsSDKDomainSwitchAndStoresSecret(t *testing.T) {
37 isolateDesktopUserDirs(t)
38 t.Cleanup(func() { _ = os.Unsetenv("LARK_BOT_APP_SECRET") })
39 pollCount := 0
40 var beginHost string
41 var pollHosts []string
42 var actions []string
43 withRewrittenHTTP(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44 if r.URL.Path != "/oauth/v1/app/registration" {
45 http.NotFound(w, r)
46 return
47 }
48 if err := r.ParseForm(); err != nil {
49 http.Error(w, err.Error(), http.StatusBadRequest)
50 return
51 }
52 switch r.Form.Get("action") {
53 case "begin":
54 beginHost = r.Header.Get("X-Test-Original-Host")
55 actions = append(actions, "begin")
56 if r.Form.Get("archetype") != "PersonalAgent" || r.Form.Get("auth_method") != "client_secret" {
57 http.Error(w, "wrong begin form", http.StatusBadRequest)
58 return
59 }
60 writeJSON(t, w, map[string]any{
61 "device_code": "dev-lark",
62 "verification_uri_complete": "https://open.feishu.cn/page/launcher?user_code=CODE",
63 "user_code": "CODE",
64 "interval": 3,
65 "expire_in": 300,
66 })
67 case "poll":
68 pollHosts = append(pollHosts, r.Header.Get("X-Test-Original-Host"))
69 actions = append(actions, "poll")
70 if r.Form.Get("device_code") != "dev-lark" {
71 http.Error(w, "wrong device code", http.StatusBadRequest)
72 return
73 }
74 pollCount++
75 if pollCount == 1 {
76 writeJSON(t, w, map[string]any{"user_info": map[string]any{"tenant_brand": "lark"}})
77 return
78 }
79 writeJSON(t, w, map[string]any{
80 "client_id": "cli-1",
81 "client_secret": "secret-1",
82 "user_info": map[string]any{"tenant_brand": "lark", "open_id": "ou-installer"},
83 })
84 default:
85 http.Error(w, "unknown action", http.StatusBadRequest)
86 }
87 }))
88
89 app := NewApp()
90 start, err := app.StartBotConnectionInstall("lark", "")
91 if err != nil {
92 t.Fatalf("StartBotConnectionInstall: %v", err)
93 }
94 if !start.OK || start.Domain != "lark" || start.InstallID == "" || start.URL == "" || start.DeviceCode != "dev-lark" {
95 t.Fatalf("start result = %+v, want ok lark-capable QR result", start)
96 }
97 qrURL, err := url.Parse(start.URL)
98 if err != nil {
99 t.Fatalf("start URL = %q, want valid QR URL: %v", start.URL, err)
100 }
101 query := qrURL.Query()
102 if query.Get("user_code") != "CODE" || query.Get("from") != "sdk" || query.Get("tp") != "sdk" || query.Get("source") != "go-sdk" {
103 t.Fatalf("start URL query = %v, want SDK registration QR metadata with user_code", query)
104 }
105 if qrURL.Host != "open.feishu.cn" {
106 t.Fatalf("start URL host = %q, want SDK Feishu launcher host", qrURL.Host)
107 }
108
109 pending, err := app.PollBotConnectionInstall(start.InstallID)
110 if err != nil {
111 t.Fatalf("PollBotConnectionInstall pending: %v", err)
112 }
113 if pending.Done || pending.Status != "pending" {
114 t.Fatalf("pending poll result = %+v, want pending domain switch", pending)
115 }
116 poll, err := app.PollBotConnectionInstall(start.InstallID)
117 if err != nil {
118 t.Fatalf("PollBotConnectionInstall: %v", err)
119 }
120 if !poll.Done {
121 t.Fatalf("poll result = %+v, want done", poll)
122 }
123 if poll.Connection.Provider != "feishu" || poll.Connection.Domain != "lark" || poll.Connection.ID != "feishu-lark" {
124 t.Fatalf("connection = %+v, want feishu-lark from tenant_brand", poll.Connection)
125 }
126 if beginHost != "accounts.feishu.cn" {
127 t.Fatalf("begin host = %q, want SDK Feishu accounts host", beginHost)
128 }
129 if got := strings.Join(pollHosts, ","); got != "accounts.feishu.cn,accounts.larksuite.com" {
130 t.Fatalf("poll hosts = %q, want Feishu poll then Lark poll", got)
131 }
132 if got := strings.Join(actions, ","); got != "begin,poll,poll" {
133 t.Fatalf("registration actions = %q, want SDK begin, domain switch, final poll", got)
134 }
135 if poll.Connection.WorkspaceRoot != "" {
136 t.Fatalf("connection workspaceRoot = %q, want empty global default", poll.Connection.WorkspaceRoot)
137 }
138 if poll.Connection.Credential.AppID != "cli-1" || poll.Connection.Credential.AppSecretEnv != "LARK_BOT_APP_SECRET" || !poll.Connection.Credential.SecretSet {
139 t.Fatalf("credential = %+v, want stored Lark secret", poll.Connection.Credential)
140 }
141 cfg := config.LoadForEdit(config.UserConfigPath())
142 if !cfg.Bot.Enabled || !cfg.Bot.Feishu.Enabled || cfg.Bot.Feishu.Domain != "lark" || cfg.Bot.Feishu.Mode != "websocket" || !cfg.Bot.Feishu.RequireMention {
143 t.Fatalf("saved feishu config = %+v, want enabled websocket lark with mention gating", cfg.Bot.Feishu)
144 }
145 if len(cfg.Bot.Allowlist.FeishuUsers) != 1 || cfg.Bot.Allowlist.FeishuUsers[0] != "ou-installer" {
146 t.Fatalf("feishu allowlist = %+v, want installer open_id", cfg.Bot.Allowlist.FeishuUsers)
147 }
148 if err := os.Unsetenv("LARK_BOT_APP_SECRET"); err != nil {
149 t.Fatalf("unset lark secret env: %v", err)
150 }
151 reloaded, err := config.Load()
152 if err != nil {
153 t.Fatalf("reload config: %v", err)
154 }
155 if got := os.Getenv("LARK_BOT_APP_SECRET"); got != "secret-1" {
156 t.Fatalf("reloaded LARK_BOT_APP_SECRET = %q, want persisted secret", got)
157 }
158 if len(reloaded.Bot.Connections) != 1 || !botConnectionView(reloaded.Bot.Connections[0]).Credential.SecretSet {
159 t.Fatalf("reloaded connections = %+v, want secret to survive restart", reloaded.Bot.Connections)
160 }
161 }
162
163 func TestFeishuInstallSwitchesToLarkDomainWhenTenantBrandIsLark(t *testing.T) {
164 isolateDesktopUserDirs(t)
165 t.Cleanup(func() { _ = os.Unsetenv("LARK_BOT_APP_SECRET") })
166 pollCount := 0
167 var beginHost string
168 var pollHosts []string
169 withRewrittenHTTP(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
170 if r.URL.Path != "/oauth/v1/app/registration" {
171 http.NotFound(w, r)
172 return
173 }
174 if err := r.ParseForm(); err != nil {
175 http.Error(w, err.Error(), http.StatusBadRequest)
176 return
177 }
178 switch r.Form.Get("action") {
179 case "begin":
180 beginHost = r.Header.Get("X-Test-Original-Host")
181 writeJSON(t, w, map[string]any{
182 "device_code": "dev-feishu",
183 "verification_uri_complete": "https://accounts.example/verify?user_code=CODE",
184 "user_code": "CODE",
185 "interval": 3,
186 "expire_in": 300,
187 })
188 case "poll":
189 pollHosts = append(pollHosts, r.Header.Get("X-Test-Original-Host"))
190 if r.Form.Get("device_code") != "dev-feishu" {
191 http.Error(w, "wrong device code", http.StatusBadRequest)
192 return
193 }
194 pollCount++
195 if pollCount == 1 {
196 writeJSON(t, w, map[string]any{"user_info": map[string]any{"tenant_brand": "lark"}})
197 return
198 }
199 writeJSON(t, w, map[string]any{
200 "client_id": "cli-lark",
201 "client_secret": "secret-lark",
202 "user_info": map[string]any{"tenant_brand": "lark", "open_id": "ou-lark-installer"},
203 })
204 default:
205 http.Error(w, "unknown action", http.StatusBadRequest)
206 }
207 }))
208
209 app := NewApp()
210 start, err := app.StartBotConnectionInstall("feishu", "")
211 if err != nil {
212 t.Fatalf("StartBotConnectionInstall: %v", err)
213 }
214 if !start.OK || start.Domain != "feishu" || start.DeviceCode != "dev-feishu" {
215 t.Fatalf("start result = %+v, want Feishu QR result", start)
216 }
217 pending, err := app.PollBotConnectionInstall(start.InstallID)
218 if err != nil {
219 t.Fatalf("PollBotConnectionInstall pending: %v", err)
220 }
221 if pending.Done || pending.Status != "pending" {
222 t.Fatalf("pending poll result = %+v, want pending domain switch", pending)
223 }
224 poll, err := app.PollBotConnectionInstall(start.InstallID)
225 if err != nil {
226 t.Fatalf("PollBotConnectionInstall: %v", err)
227 }
228 if !poll.Done || poll.Connection.Domain != "lark" || poll.Connection.Credential.AppSecretEnv != "LARK_BOT_APP_SECRET" {
229 t.Fatalf("poll result = %+v, want stored Lark connection after domain switch", poll)
230 }
231 if beginHost != "accounts.feishu.cn" {
232 t.Fatalf("begin host = %q, want Feishu accounts host", beginHost)
233 }
234 if got := strings.Join(pollHosts, ","); got != "accounts.feishu.cn,accounts.larksuite.com" {
235 t.Fatalf("poll hosts = %q, want Feishu poll then Lark poll", got)
236 }
237 }
238
239 func TestFeishuInstallStoresFeishuSecretAndSurvivesReload(t *testing.T) {
240 isolateDesktopUserDirs(t)
241 t.Cleanup(func() { _ = os.Unsetenv("FEISHU_BOT_APP_SECRET") })
242 var hosts []string
243 withRewrittenHTTP(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
244 if r.URL.Path != "/oauth/v1/app/registration" {
245 http.NotFound(w, r)
246 return
247 }
248 if err := r.ParseForm(); err != nil {
249 http.Error(w, err.Error(), http.StatusBadRequest)
250 return
251 }
252 hosts = append(hosts, r.Form.Get("action")+":"+r.Header.Get("X-Test-Original-Host"))
253 switch r.Form.Get("action") {
254 case "begin":
255 writeJSON(t, w, map[string]any{
256 "device_code": "dev-feishu",
257 "verification_uri_complete": "https://accounts.example/verify?user_code=CODE",
258 "user_code": "CODE",
259 "interval": 3,
260 "expire_in": 300,
261 })
262 case "poll":
263 writeJSON(t, w, map[string]any{
264 "client_id": "cli-feishu",
265 "client_secret": "secret-feishu",
266 "user_info": map[string]any{"tenant_brand": "feishu", "open_id": "ou-feishu-installer"},
267 })
268 default:
269 http.Error(w, "unknown action", http.StatusBadRequest)
270 }
271 }))
272
273 app := NewApp()
274 start, err := app.StartBotConnectionInstall("feishu", "")
275 if err != nil {
276 t.Fatalf("StartBotConnectionInstall: %v", err)
277 }
278 if !start.OK || start.Domain != "feishu" || start.InstallID == "" {
279 t.Fatalf("start result = %+v, want ok Feishu QR result", start)
280 }
281 poll, err := app.PollBotConnectionInstall(start.InstallID)
282 if err != nil {
283 t.Fatalf("PollBotConnectionInstall: %v", err)
284 }
285 if !poll.Done {
286 t.Fatalf("poll result = %+v, want done", poll)
287 }
288 if poll.Connection.Provider != "feishu" || poll.Connection.Domain != "feishu" || poll.Connection.ID != "feishu-feishu" {
289 t.Fatalf("connection = %+v, want feishu-feishu", poll.Connection)
290 }
291 if poll.Connection.Credential.AppID != "cli-feishu" || poll.Connection.Credential.AppSecretEnv != "FEISHU_BOT_APP_SECRET" || !poll.Connection.Credential.SecretSet {
292 t.Fatalf("credential = %+v, want stored Feishu secret", poll.Connection.Credential)
293 }
294 if got := strings.Join(hosts, ","); got != "begin:accounts.feishu.cn,poll:accounts.feishu.cn" {
295 t.Fatalf("registration hosts = %q, want Feishu begin and poll", got)
296 }
297 cfg := config.LoadForEdit(config.UserConfigPath())
298 if !cfg.Bot.Enabled || !cfg.Bot.Feishu.Enabled || cfg.Bot.Feishu.Domain != "feishu" || cfg.Bot.Feishu.AppID != "cli-feishu" {
299 t.Fatalf("saved feishu config = %+v, want enabled Feishu websocket config", cfg.Bot.Feishu)
300 }
301 if len(cfg.Bot.Allowlist.FeishuUsers) != 1 || cfg.Bot.Allowlist.FeishuUsers[0] != "ou-feishu-installer" {
302 t.Fatalf("feishu allowlist = %+v, want installer open_id", cfg.Bot.Allowlist.FeishuUsers)
303 }
304 if err := os.Unsetenv("FEISHU_BOT_APP_SECRET"); err != nil {
305 t.Fatalf("unset feishu secret env: %v", err)
306 }
307 reloaded, err := config.Load()
308 if err != nil {
309 t.Fatalf("reload config: %v", err)
310 }
311 if got := os.Getenv("FEISHU_BOT_APP_SECRET"); got != "secret-feishu" {
312 t.Fatalf("reloaded FEISHU_BOT_APP_SECRET = %q, want persisted secret", got)
313 }
314 if len(reloaded.Bot.Connections) != 1 || !botConnectionView(reloaded.Bot.Connections[0]).Credential.SecretSet {
315 t.Fatalf("reloaded connections = %+v, want secret to survive restart", reloaded.Bot.Connections)
316 }
317 }
318
319 func TestWeixinInstallStoresSavedAccountAndConnection(t *testing.T) {
320 isolateDesktopUserDirs(t)
321 withRewrittenHTTP(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
322 switch r.URL.Path {
323 case "/ilink/bot/get_bot_qrcode":
324 if r.URL.Query().Get("bot_type") != "3" {
325 http.Error(w, "missing bot type", http.StatusBadRequest)
326 return
327 }
328 writeJSON(t, w, map[string]any{
329 "qrcode": "qr-weixin",
330 "qrcode_img_content": "data:image/png;base64,abc",
331 })
332 case "/ilink/bot/get_qrcode_status":
333 if r.URL.Query().Get("qrcode") != "qr-weixin" {
334 http.Error(w, "wrong qr", http.StatusBadRequest)
335 return
336 }
337 writeJSON(t, w, map[string]any{
338 "status": "confirmed",
339 "ilink_bot_id": "weixin-account",
340 "bot_token": "token-1",
341 "ilink_user_id": "user-1",
342 "baseurl": "https://ilinkai.weixin.qq.com",
343 })
344 default:
345 http.NotFound(w, r)
346 }
347 }))
348
349 app := NewApp()
350 start, err := app.StartBotConnectionInstall("weixin", "")
351 if err != nil {
352 t.Fatalf("StartBotConnectionInstall: %v", err)
353 }
354 if !start.OK || start.Provider != "weixin" || start.Domain != "weixin" || start.URL != "data:image/png;base64,abc" || start.DeviceCode != "qr-weixin" {
355 t.Fatalf("start result = %+v, want weixin QR result", start)
356 }
357
358 poll, err := app.PollBotConnectionInstall(start.InstallID)
359 if err != nil {
360 t.Fatalf("PollBotConnectionInstall: %v", err)
361 }
362 if !poll.Done {
363 t.Fatalf("poll result = %+v, want done", poll)
364 }
365 if poll.Connection.Provider != "weixin" || poll.Connection.Domain != "weixin" || poll.Connection.Credential.AccountID != "weixin-account" {
366 t.Fatalf("connection = %+v, want weixin account connection", poll.Connection)
367 }
368 if poll.Connection.WorkspaceRoot != "" {
369 t.Fatalf("connection workspaceRoot = %q, want empty global default", poll.Connection.WorkspaceRoot)
370 }
371 if poll.Connection.Credential.TokenEnv != "WEIXIN_BOT_TOKEN" || !poll.Connection.Credential.SecretSet {
372 t.Fatalf("credential = %+v, want saved account to count as configured token", poll.Connection.Credential)
373 }
374 cfg := config.LoadForEdit(config.UserConfigPath())
375 if !cfg.Bot.Enabled || !cfg.Bot.Weixin.Enabled || cfg.Bot.Weixin.AccountID != "weixin-account" || cfg.Bot.Weixin.TokenEnv != "WEIXIN_BOT_TOKEN" {
376 t.Fatalf("saved weixin config = %+v, want enabled saved account", cfg.Bot.Weixin)
377 }
378 if len(cfg.Bot.Allowlist.WeixinUsers) != 1 || cfg.Bot.Allowlist.WeixinUsers[0] != "user-1" {
379 t.Fatalf("weixin allowlist = %+v, want installer user id", cfg.Bot.Allowlist.WeixinUsers)
380 }
381 reloaded, err := config.Load()
382 if err != nil {
383 t.Fatalf("reload config: %v", err)
384 }
385 if len(reloaded.Bot.Connections) != 1 {
386 t.Fatalf("reloaded connections = %+v, want saved weixin connection", reloaded.Bot.Connections)
387 }
388 reloadedConnection := botConnectionView(reloaded.Bot.Connections[0])
389 if reloadedConnection.Credential.AccountID != "weixin-account" || !reloadedConnection.Credential.SecretSet {
390 t.Fatalf("reloaded credential = %+v, want saved weixin account to survive restart", reloadedConnection.Credential)
391 }
392 }
393
394 func TestFeishuRegistrationQRCodeURLAddsSDKMetadata(t *testing.T) {
395 qrURL, err := feishuRegistrationQRCodeURL("https://open.larksuite.com/page/launcher?user_code=ABCD-1234&source=old")
396 if err != nil {
397 t.Fatalf("feishuRegistrationQRCodeURL: %v", err)
398 }
399 parsed, err := url.Parse(qrURL)
400 if err != nil {
401 t.Fatalf("parse QR URL: %v", err)
402 }
403 query := parsed.Query()
404 if query.Get("user_code") != "ABCD-1234" {
405 t.Fatalf("user_code = %q, want preserved code", query.Get("user_code"))
406 }
407 if query.Get("from") != "sdk" || query.Get("tp") != "sdk" || query.Get("source") != "go-sdk" {
408 t.Fatalf("query = %v, want SDK registration metadata", query)
409 }
410 }
411
412 func TestDiagnoseBotConnectionBuildsReportDetailForMissingSecret(t *testing.T) {
413 isolateDesktopUserDirs(t)
414 t.Setenv("FEISHU_BOT_APP_SECRET_PRIVATE", "")
415 app := NewApp()
416 if _, err := app.upsertBotConnection(config.BotConnectionConfig{
417 ID: "feishu-lark",
418 Provider: "feishu",
419 Domain: "lark",
420 Label: "Lark",
421 Enabled: true,
422 Status: "connected",
423 WorkspaceRoot: "/Users/alice/work/reasonix",
424 Credential: config.BotConnectionCredential{
425 AppID: "cli-private",
426 AppSecretEnv: "FEISHU_BOT_APP_SECRET_PRIVATE",
427 },
428 SessionMappings: []config.BotConnectionSessionMapping{{
429 RemoteID: "ou-private",
430 SessionID: "session-private",
431 Scope: "project",
432 WorkspaceRoot: "/Users/alice/work/reasonix",
433 }},
434 }, nil); err != nil {
435 t.Fatalf("upsert connection: %v", err)
436 }
437
438 diag, err := app.DiagnoseBotConnection("feishu-lark")
439 if err != nil {
440 t.Fatalf("DiagnoseBotConnection: %v", err)
441 }
442 if diag.Status != "warning" || diag.Phase != "credential" || diag.Code != "secret_missing" || diag.ReportKind != "bot" || diag.ReportDetail == "" {
443 t.Fatalf("diagnostic = %+v, want warning credential report", diag)
444 }
445 for _, leaked := range []string{"FEISHU_BOT_APP_SECRET_PRIVATE", "/Users/alice", "ou-private", "session-private"} {
446 if strings.Contains(diag.ReportDetail, leaked) {
447 t.Fatalf("diagnostic report leaked %q in %s", leaked, diag.ReportDetail)
448 }
449 }
450 var payload frontendCrashPayload
451 if err := json.Unmarshal([]byte(diag.ReportDetail), &payload); err != nil {
452 t.Fatalf("report detail is not structured JSON: %v", err)
453 }
454 if payload.Kind != "bot" || payload.Source != "bot.runtime" || payload.Label != "bot.feishu.lark.credential" {
455 t.Fatalf("payload = %+v, want bot runtime credential label", payload)
456 }
457 for _, want := range []string{
458 "app_secret_env_configured: true",
459 "secret_available: false",
460 "workspace_scope: project",
461 "session_mappings: 1",
462 "summary: required bot credential is not available",
463 } {
464 if !strings.Contains(payload.Message, want) {
465 t.Fatalf("payload message = %q, want it to contain %q", payload.Message, want)
466 }
467 }
468 report, err := crashReportFromDetail(diag.ReportKind, diag.ReportDetail)
469 if err != nil {
470 t.Fatalf("crashReportFromDetail: %v", err)
471 }
472 if report.Kind != "bot" || report.Source != "bot.runtime" || report.ErrorType != "BotConnectionDiagnostic" {
473 t.Fatalf("report = %+v, want accepted bot report", report)
474 }
475 }
476
477 func TestBotConnectionSendFailureReportRedactsEnvNames(t *testing.T) {
478 conn := config.BotConnectionConfig{
479 ID: "feishu-lark",
480 Provider: "feishu",
481 Domain: "lark",
482 Label: "Lark",
483 Enabled: true,
484 Status: "connected",
485 Credential: config.BotConnectionCredential{
486 AppSecretEnv: "FEISHU_BOT_APP_SECRET_PRIVATE",
487 },
488 }
489 diag := botConnectionDiagnostic(&conn, conn.ID, "error", "send", "test_send_failed", "feishu app_id or FEISHU_BOT_APP_SECRET_PRIVATE is not configured", true)
490 if diag.ReportKind != "bot" || diag.ReportDetail == "" {
491 t.Fatalf("diagnostic = %+v, want reportable bot diagnostic", diag)
492 }
493 if strings.Contains(diag.ReportDetail, "FEISHU_BOT_APP_SECRET_PRIVATE") {
494 t.Fatalf("diagnostic report leaked env name in %s", diag.ReportDetail)
495 }
496 var payload frontendCrashPayload
497 if err := json.Unmarshal([]byte(diag.ReportDetail), &payload); err != nil {
498 t.Fatalf("report detail is not structured JSON: %v", err)
499 }
500 if !strings.Contains(payload.ErrorMessage, "[redacted-env]") {
501 t.Fatalf("payload errorMessage = %q, want redacted env marker", payload.ErrorMessage)
502 }
503 }
504
505 func TestDiagnoseWeixinConnectionDetectsMissingSavedAccountWithoutTokenEnv(t *testing.T) {
506 isolateDesktopUserDirs(t)
507 app := NewApp()
508 if _, err := app.upsertBotConnection(config.BotConnectionConfig{
509 ID: "weixin-weixin",
510 Provider: "weixin",
511 Domain: "weixin",
512 Label: "微信",
513 Enabled: true,
514 Status: "connected",
515 Credential: config.BotConnectionCredential{
516 AccountID: "missing-account",
517 },
518 }, nil); err != nil {
519 t.Fatalf("upsert connection: %v", err)
520 }
521
522 diag, err := app.DiagnoseBotConnection("weixin-weixin")
523 if err != nil {
524 t.Fatalf("DiagnoseBotConnection: %v", err)
525 }
526 if diag.Status != "warning" || diag.Phase != "credential" || diag.Code != "secret_missing" || diag.ReportKind != "bot" || diag.ReportDetail == "" {
527 t.Fatalf("diagnostic = %+v, want missing local credential warning", diag)
528 }
529 if strings.Contains(diag.ReportDetail, "missing-account") {
530 t.Fatalf("diagnostic report leaked account id in %s", diag.ReportDetail)
531 }
532 }
533
534 func TestRememberBotConnectionRemoteStoresStableScope(t *testing.T) {
535 isolateDesktopUserDirs(t)
536 app := NewApp()
537 if _, err := app.upsertBotConnection(config.BotConnectionConfig{
538 ID: "feishu-lark",
539 Provider: "feishu",
540 Domain: "lark",
541 Label: "kun",
542 Enabled: true,
543 Status: "connected",
544 }, nil); err != nil {
545 t.Fatalf("upsert global connection: %v", err)
546 }
547 if err := app.rememberBotConnectionRemote("feishu-lark", "ou_global"); err != nil {
548 t.Fatalf("remember global remote: %v", err)
549 }
550 cfg := config.LoadForEdit(config.UserConfigPath())
551 if got := cfg.Bot.Connections[0].SessionMappings[0]; got.Scope != "global" || got.WorkspaceRoot != "" || got.RemoteID != "ou_global" {
552 t.Fatalf("global mapping = %+v, want scope=global without workspace", got)
553 }
554
555 if _, err := app.upsertBotConnection(config.BotConnectionConfig{
556 ID: "weixin-project",
557 Provider: "weixin",
558 Domain: "weixin",
559 Label: "project",
560 Enabled: true,
561 Status: "connected",
562 WorkspaceRoot: "/tmp/reasonix-project",
563 }, nil); err != nil {
564 t.Fatalf("upsert project connection: %v", err)
565 }
566 if err := app.rememberBotConnectionRemote("weixin-project", "wxid_project"); err != nil {
567 t.Fatalf("remember project remote: %v", err)
568 }
569 cfg = config.LoadForEdit(config.UserConfigPath())
570 var projectMapping config.BotConnectionSessionMapping
571 for _, conn := range cfg.Bot.Connections {
572 if conn.ID == "weixin-project" && len(conn.SessionMappings) == 1 {
573 projectMapping = conn.SessionMappings[0]
574 }
575 }
576 if projectMapping.Scope != "project" || projectMapping.WorkspaceRoot != "/tmp/reasonix-project" || projectMapping.RemoteID != "wxid_project" {
577 t.Fatalf("project mapping = %+v, want project scope and workspace", projectMapping)
578 }
579 }
580
581 func writeJSON(t *testing.T, w http.ResponseWriter, value any) {
582 t.Helper()
583 w.Header().Set("Content-Type", "application/json")
584 if err := json.NewEncoder(w).Encode(value); err != nil {
585 t.Fatalf("write json: %v", err)
586 }
587 }
588
589 func withRewrittenHTTP(t *testing.T, handler http.Handler) {
590 t.Helper()
591 server := httptest.NewServer(handler)
592 target, err := url.Parse(server.URL)
593 if err != nil {
594 t.Fatal(err)
595 }
596 previous := http.DefaultTransport
597 http.DefaultTransport = rewriteHTTPTransport{target: target, next: previous}
598 t.Cleanup(func() {
599 http.DefaultTransport = previous
600 server.Close()
601 })
602 }
603
604 type rewriteHTTPTransport struct {
605 target *url.URL
606 next http.RoundTripper
607 }
608
609 func (r rewriteHTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) {
610 clone := req.Clone(req.Context())
611 clone.Header.Set("X-Test-Original-Host", req.URL.Host)
612 clone.URL.Scheme = r.target.Scheme
613 clone.URL.Host = r.target.Host
614 clone.Host = r.target.Host
615 if r.next == nil {
616 r.next = http.DefaultTransport
617 }
618 clone.URL.Path = "/" + strings.TrimLeft(clone.URL.Path, "/")
619 return r.next.RoundTrip(clone)
620 }
621
621 lines GO