mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 10:15:48 +00:00
- internal/providers/acp 0.0% → 80.0% (7 test files, ~2560 LOC)
- helpers, types round-trip, jsonrpc framing + adversarial fuzz,
session, terminal/sandbox, tool_bridge (3 permission modes),
ProcessPool lifecycle
- internal/channels/feishu 20.6% → 63.9% (15 test files)
- bot parse/policy, factory, larkws proto + WS lifecycle,
larkclient HTTP error paths, larkevents AES-CBC decrypt +
tamper detection, media send/receive, lifecycle
Zero source modifications. Race clean (-race -count=3).
Dual build verified (standard + sqliteonly).
280 lines
8.4 KiB
Go
280 lines
8.4 KiB
Go
package feishu
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/bus"
|
|
)
|
|
|
|
// --- sendText error path ---
|
|
|
|
func TestSendText_Error(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":10001,"msg":"error","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
err := ch.sendText(context.Background(), "oc_chat_1", "chat_id", "hello", "")
|
|
if err == nil {
|
|
t.Fatal("expected error for non-zero API code")
|
|
}
|
|
}
|
|
|
|
// --- sendMarkdownCard error path ---
|
|
|
|
func TestSendMarkdownCard_Error(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":10001,"msg":"error","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
err := ch.sendMarkdownCard(context.Background(), "oc_chat_1", "chat_id", "text", "", nil)
|
|
if err == nil {
|
|
t.Fatal("expected error for non-zero API code")
|
|
}
|
|
}
|
|
|
|
// --- sendChunkedText error propagation ---
|
|
|
|
func TestSendChunkedText_Error(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":10001,"msg":"error","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
err := ch.sendChunkedText(context.Background(), "oc_chat_1", "chat_id", "hello error test", 4000, "")
|
|
if err == nil {
|
|
t.Fatal("expected error propagated from sendText")
|
|
}
|
|
}
|
|
|
|
// --- sendImage error path ---
|
|
|
|
func TestSendImage_Error(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":10001,"msg":"error","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
err := ch.sendImage(context.Background(), "oc_chat_1", "chat_id", "img_key_abc", "")
|
|
if err == nil {
|
|
t.Fatal("expected error for send image failure")
|
|
}
|
|
}
|
|
|
|
// --- sendFile error path ---
|
|
|
|
func TestSendFile_Error(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":10001,"msg":"error","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
err := ch.sendFile(context.Background(), "oc_chat_1", "chat_id", "file_key_abc", "file", "")
|
|
if err == nil {
|
|
t.Fatal("expected error for send file failure")
|
|
}
|
|
}
|
|
|
|
// --- OnReactionEvent active state (thinking/tool) ---
|
|
|
|
func TestOnReactionEvent_Thinking_AddsReaction(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{"reaction_id":"rx_001"}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
ch.cfg.ReactionLevel = "full"
|
|
|
|
err := ch.OnReactionEvent(context.Background(), "oc_chat_1", "om_msg_1", "thinking")
|
|
if err != nil {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestOnReactionEvent_Thinking_AlreadyHasReaction(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{"reaction_id":"rx_001"}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
ch.cfg.ReactionLevel = "full"
|
|
|
|
// First call stores the reaction
|
|
_ = ch.OnReactionEvent(context.Background(), "oc_chat_11", "om_msg_1", "thinking")
|
|
|
|
// Second call with same chatID: already loaded, should skip
|
|
err := ch.OnReactionEvent(context.Background(), "oc_chat_11", "om_msg_2", "thinking")
|
|
if err != nil {
|
|
t.Errorf("unexpected error on second thinking call: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- removeTypingReaction with stored reaction ---
|
|
|
|
func TestRemoveTypingReaction_WithStoredReaction(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
ch.reactions.Store("oc_chat_2", &reactionState{
|
|
messageID: "om_msg_2",
|
|
reactionID: "rx_stored_1",
|
|
})
|
|
|
|
err := ch.removeTypingReaction(context.Background(), "oc_chat_2")
|
|
if err != nil {
|
|
t.Errorf("removeTypingReaction: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRemoveTypingReaction_EmptyReactionID(t *testing.T) {
|
|
ch := &Channel{}
|
|
// Store state with empty reactionID — should be no-op after LoadAndDelete
|
|
ch.reactions.Store("oc_chat_3", &reactionState{
|
|
messageID: "om_msg_3",
|
|
reactionID: "",
|
|
})
|
|
|
|
err := ch.removeTypingReaction(context.Background(), "oc_chat_3")
|
|
if err != nil {
|
|
t.Errorf("unexpected error for empty reactionID: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- ClearReaction with stored reaction ---
|
|
|
|
func TestClearReaction_WithStoredReaction(t *testing.T) {
|
|
srv := newSimpleMockServer(t, `{"code":0,"msg":"ok","data":{}}`)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
ch.reactions.Store("oc_chat_4", &reactionState{
|
|
messageID: "om_msg_4",
|
|
reactionID: "rx_to_clear",
|
|
})
|
|
|
|
err := ch.ClearReaction(context.Background(), "oc_chat_4", "")
|
|
if err != nil {
|
|
t.Errorf("ClearReaction: %v", err)
|
|
}
|
|
}
|
|
|
|
// --- WebhookHandler returns handler in webhook mode with port=0 ---
|
|
|
|
func TestWebhookHandler_WebhookNoPort_ReturnsHandler(t *testing.T) {
|
|
ch := &Channel{}
|
|
ch.cfg.ConnectionMode = "webhook"
|
|
ch.cfg.WebhookPort = 0
|
|
ch.cfg.VerificationToken = "testtoken"
|
|
|
|
path, handler := ch.WebhookHandler()
|
|
if path == "" {
|
|
t.Error("expected non-empty path for webhook mode with no port")
|
|
}
|
|
if handler == nil {
|
|
t.Error("expected non-nil handler for webhook mode with no port")
|
|
}
|
|
}
|
|
|
|
func TestWebhookHandler_CustomPath_ReturnsHandler(t *testing.T) {
|
|
ch := &Channel{}
|
|
ch.cfg.ConnectionMode = "webhook"
|
|
ch.cfg.WebhookPort = 0
|
|
ch.cfg.WebhookPath = "/custom/feishu"
|
|
|
|
path, handler := ch.WebhookHandler()
|
|
if path != "/custom/feishu" {
|
|
t.Errorf("expected /custom/feishu, got %q", path)
|
|
}
|
|
if handler == nil {
|
|
t.Error("expected non-nil handler")
|
|
}
|
|
}
|
|
|
|
// --- sendMediaAttachment ---
|
|
|
|
// newDualResponseServer returns a server that gives resp1 on first non-token request,
|
|
// resp2 on subsequent requests.
|
|
func newDualResponseServer(t *testing.T, resp1, resp2 string) *httptest.Server {
|
|
t.Helper()
|
|
call := 0
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == tokenEndpoint {
|
|
w.Write([]byte(`{"code":0,"msg":"ok","tenant_access_token":"tok","expire":7200}`))
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if call == 0 {
|
|
w.Write([]byte(resp1))
|
|
} else {
|
|
w.Write([]byte(resp2))
|
|
}
|
|
call++
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv
|
|
}
|
|
|
|
func TestSendMediaAttachment_Image(t *testing.T) {
|
|
srv := newDualResponseServer(t,
|
|
`{"code":0,"msg":"ok","data":{"image_key":"img_test_key"}}`,
|
|
`{"code":0,"msg":"ok","data":{"message_id":"om_img_x"}}`,
|
|
)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "test_img_*.png")
|
|
if err != nil {
|
|
t.Fatalf("create temp: %v", err)
|
|
}
|
|
f.Write([]byte{0x89, 0x50, 0x4E, 0x47})
|
|
f.Close()
|
|
|
|
att := bus.MediaAttachment{URL: f.Name(), ContentType: "image/png"}
|
|
if err := ch.sendMediaAttachment(context.Background(), "oc_chat", "chat_id", att, ""); err != nil {
|
|
t.Errorf("sendMediaAttachment image: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMediaAttachment_EmptyURL(t *testing.T) {
|
|
ch := &Channel{}
|
|
att := bus.MediaAttachment{URL: "", ContentType: "image/png"}
|
|
if err := ch.sendMediaAttachment(context.Background(), "oc_chat", "chat_id", att, ""); err != nil {
|
|
t.Errorf("expected no error for empty URL: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMediaAttachment_DefaultFile(t *testing.T) {
|
|
srv := newDualResponseServer(t,
|
|
`{"code":0,"msg":"ok","data":{"file_key":"file_test_key"}}`,
|
|
`{"code":0,"msg":"ok","data":{"message_id":"om_file_x"}}`,
|
|
)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "test_doc_*.pdf")
|
|
if err != nil {
|
|
t.Fatalf("create temp: %v", err)
|
|
}
|
|
f.Write([]byte("pdf data"))
|
|
f.Close()
|
|
|
|
att := bus.MediaAttachment{URL: f.Name(), ContentType: "application/pdf"}
|
|
if err := ch.sendMediaAttachment(context.Background(), "oc_chat", "chat_id", att, ""); err != nil {
|
|
t.Errorf("sendMediaAttachment file: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMediaAttachment_Video(t *testing.T) {
|
|
srv := newDualResponseServer(t,
|
|
`{"code":0,"msg":"ok","data":{"file_key":"video_test_key"}}`,
|
|
`{"code":0,"msg":"ok","data":{"message_id":"om_video_x"}}`,
|
|
)
|
|
ch := &Channel{client: NewLarkClient("app", "secret", srv.URL)}
|
|
|
|
f, err := os.CreateTemp(t.TempDir(), "test_vid_*.mp4")
|
|
if err != nil {
|
|
t.Fatalf("create temp: %v", err)
|
|
}
|
|
f.Write([]byte("video data"))
|
|
f.Close()
|
|
|
|
att := bus.MediaAttachment{URL: f.Name(), ContentType: "video/mp4"}
|
|
if err := ch.sendMediaAttachment(context.Background(), "oc_chat", "chat_id", att, ""); err != nil {
|
|
t.Errorf("sendMediaAttachment video: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSendMediaAttachment_FileNotFound(t *testing.T) {
|
|
ch := &Channel{}
|
|
att := bus.MediaAttachment{URL: "/nonexistent/path/file.pdf", ContentType: "application/pdf"}
|
|
if err := ch.sendMediaAttachment(context.Background(), "oc_chat", "chat_id", att, ""); err == nil {
|
|
t.Fatal("expected error for missing file")
|
|
}
|
|
}
|