package feishu import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" ) func testCard() *ResignCard { return &ResignCard{ DisplayName: "张三", UID: "zhangsan", Reason: "飞书通讯录中已不可见,经机器确认已离职。", ConfirmURL: "https://confirm.example.com/api/resign/confirm?uid=zhangsan&token=abc123", } } // TestBuildResignCardPayload 验证卡片载荷基本结构完整,且包含了姓名/uid/离职依据/确认链接。 func TestBuildResignCardPayload(t *testing.T) { raw, err := BuildResignCardPayload(testCard()) if err != nil { t.Fatalf("构造卡片载荷失败: %v", err) } var doc struct { MsgType string `json:"msg_type"` Card struct { Header struct { Title struct { Content string `json:"content"` } `json:"title"` } `json:"header"` Elements []map[string]any `json:"elements"` } `json:"card"` } if err := json.Unmarshal(raw, &doc); err != nil { t.Fatalf("卡片载荷不是合法 JSON: %v", err) } if doc.MsgType != "interactive" { t.Errorf("msg_type = %q, 期望 interactive", doc.MsgType) } if doc.Card.Header.Title.Content != "离职确认" { t.Errorf("卡片标题 = %q, 期望 离职确认", doc.Card.Header.Title.Content) } // 找到 div 里的正文与 action 里的确认按钮。 joined := string(raw) for _, want := range []string{"张三", "zhangsan", "飞书通讯录中已不可见", "https://confirm.example.com"} { if !strings.Contains(joined, want) { t.Errorf("卡片载荷缺少关键内容 %q", want) } } } // TestBuildResignCardPayloadRejectsMissingConfirmURL 验证没有确认链接时不构造卡片。 // 卡片按钮是该流程的唯一确认入口,缺失链接等于拿掉半道闸,必须拒绝。 func TestBuildResignCardPayloadRejectsMissingConfirmURL(t *testing.T) { c := testCard() c.ConfirmURL = "" if _, err := BuildResignCardPayload(c); err == nil { t.Errorf("缺少确认链接时不应构造卡片,却成功了") } } // TestSendResignCardSuccess 验证向本地假 webhook 发送成功并携带正确载荷。 func TestSendResignCardSuccess(t *testing.T) { var received []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { t.Errorf("请求方法 = %s, 期望 POST", r.Method) } if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") { t.Errorf("Content-Type = %q", ct) } received, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"StatusCode":0,"StatusMessage":"success"}`)) })) defer srv.Close() payload, _ := BuildResignCardPayload(testCard()) if err := SendResignCard(context.Background(), srv.URL, payload); err != nil { t.Fatalf("发送失败: %v", err) } if len(received) == 0 { t.Errorf("webhook 未收到请求体") } if string(received) != string(payload) { t.Errorf("webhook 收到的载荷与构造的不一致") } } // TestSendResignCardWebhookError 验证 webhook 返回错误码时上报失败。 func TestSendResignCardWebhookError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"code":9499,"msg":"openapi signature error"}`)) })) defer srv.Close() payload, _ := BuildResignCardPayload(testCard()) err := SendResignCard(context.Background(), srv.URL, payload) if err == nil { t.Fatalf("webhook 返回错误码应上报失败,却成功了") } if !strings.Contains(err.Error(), "9499") { t.Errorf("错误信息应包含错误码,实际: %v", err) } } // TestSendResignCardRejectsEmptyURL 验证未配置 webhook 地址时报错。 func TestSendResignCardRejectsEmptyURL(t *testing.T) { payload, _ := BuildResignCardPayload(testCard()) if err := SendResignCard(context.Background(), "", payload); err == nil { t.Errorf("空 webhook 地址应报错,却成功了") } } // TestSendResignCardRejectsNon2xx 验证 HTTP 非 200 时上报失败。 func TestSendResignCardRejectsNon2xx(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) })) defer srv.Close() payload, _ := BuildResignCardPayload(testCard()) if err := SendResignCard(context.Background(), srv.URL, payload); err == nil { t.Errorf("HTTP 400 应上报失败,却成功了") } }