package service import ( "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gochat/gochat/internal/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // ========== Create ========== func TestCaptainCustomToolService_Create_成功(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) req := &CreateCustomToolRequest{ Title: "天气查询", Slug: "weather-query", Description: "查询天气信息", EndpointURL: "https://api.weather.com/v1/current", HTTPMethod: "POST", AuthType: "none", } tool, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.NotZero(t, tool.ID) assert.Equal(t, account.ID, tool.AccountID) assert.Equal(t, "天气查询", tool.Title) assert.Equal(t, "weather-query", tool.Slug) assert.Equal(t, "POST", tool.HTTPMethod) assert.Equal(t, model.ToolAuthTypeNone, tool.AuthType) assert.True(t, tool.Enabled) } func TestCaptainCustomToolService_Create_默认值(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 不设置 HTTPMethod 和 AuthType,应使用默认值 req := &CreateCustomToolRequest{ Title: "默认工具", Slug: "default-tool", EndpointURL: "https://api.example.com", } tool, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.Equal(t, "GET", tool.HTTPMethod) // 默认 GET assert.Equal(t, model.ToolAuthTypeNone, tool.AuthType) // 默认 none } func TestCaptainCustomToolService_Create_带认证配置(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) authConfig := json.RawMessage(`{"username":"user1","password":"pass1"}`) req := &CreateCustomToolRequest{ Title: "带认证工具", Slug: "auth-tool", EndpointURL: "https://api.example.com/auth", HTTPMethod: "POST", AuthType: "basic", AuthConfig: authConfig, } tool, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.Equal(t, model.ToolAuthTypeBasic, tool.AuthType) assert.Equal(t, authConfig, tool.AuthConfig) } func TestCaptainCustomToolService_Create_带模板(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) reqTemplate := `{"query":"{{.keyword}}","location":"{{.location}}"}` respTemplate := `{"result":"{{.data.answer}}"}` paramSchema := json.RawMessage(`{"type":"object","properties":{"keyword":{"type":"string"},"location":{"type":"string"}}}`) req := &CreateCustomToolRequest{ Title: "模板工具", Slug: "template-tool", EndpointURL: "https://api.example.com/search", RequestTemplate: reqTemplate, ResponseTemplate: respTemplate, ParamSchema: paramSchema, } tool, err := svc.Create(context.Background(), account.ID, req) require.NoError(t, err) assert.Equal(t, reqTemplate, tool.RequestTemplate) assert.Equal(t, respTemplate, tool.ResponseTemplate) assert.Equal(t, paramSchema, tool.ParamSchema) } // ========== Get ========== func TestCaptainCustomToolService_Get_成功(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) tool := createTestCaptainCustomTool(t, db, account.ID) result, err := svc.Get(context.Background(), tool.ID) require.NoError(t, err) assert.Equal(t, tool.ID, result.ID) assert.Equal(t, tool.Title, result.Title) assert.Equal(t, tool.Slug, result.Slug) } func TestCaptainCustomToolService_Get_不存在(t *testing.T) { _, _, svc := setupCaptainCustomToolService(t) result, err := svc.Get(context.Background(), 9999) assert.Error(t, err) assert.Nil(t, result) } // ========== Update ========== func TestCaptainCustomToolService_Update_部分更新(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) tool := createTestCaptainCustomTool(t, db, account.ID) newTitle := "更新后的工具" req := &UpdateCustomToolRequest{ Title: newTitle, } result, err := svc.Update(context.Background(), tool.ID, req) require.NoError(t, err) assert.Equal(t, newTitle, result.Title) // 其他字段保持不变 assert.Equal(t, tool.Slug, result.Slug) assert.Equal(t, tool.EndpointURL, result.EndpointURL) } func TestCaptainCustomToolService_Update_禁用工具(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) tool := createTestCaptainCustomTool(t, db, account.ID) disabled := false req := &UpdateCustomToolRequest{ Enabled: &disabled, } result, err := svc.Update(context.Background(), tool.ID, req) require.NoError(t, err) assert.False(t, result.Enabled) } func TestCaptainCustomToolService_Update_启用工具(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 创建一个已禁用的工具 tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.Enabled = false }) enabled := true req := &UpdateCustomToolRequest{ Enabled: &enabled, } result, err := svc.Update(context.Background(), tool.ID, req) require.NoError(t, err) assert.True(t, result.Enabled) } func TestCaptainCustomToolService_Update_更新认证配置(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) tool := createTestCaptainCustomTool(t, db, account.ID) newAuthConfig := json.RawMessage(`{"token":"new-bearer-token"}`) req := &UpdateCustomToolRequest{ AuthType: "bearer", AuthConfig: newAuthConfig, } result, err := svc.Update(context.Background(), tool.ID, req) require.NoError(t, err) assert.Equal(t, model.ToolAuthTypeBearer, result.AuthType) assert.Equal(t, newAuthConfig, result.AuthConfig) } func TestCaptainCustomToolService_Update_不存在(t *testing.T) { _, _, svc := setupCaptainCustomToolService(t) req := &UpdateCustomToolRequest{Title: "不存在的工具"} result, err := svc.Update(context.Background(), 9999, req) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "not found") } func TestCaptainCustomToolService_Update_忽略nullAuthConfig(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) authConfig := json.RawMessage(`{"token":"original-token"}`) tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.AuthType = model.ToolAuthTypeBearer t.AuthConfig = authConfig }) // 传入 "null" JSON,不应覆盖原值 req := &UpdateCustomToolRequest{ AuthConfig: json.RawMessage(`null`), } result, err := svc.Update(context.Background(), tool.ID, req) require.NoError(t, err) assert.Equal(t, authConfig, result.AuthConfig) // 保持原值 } // ========== Delete ========== func TestCaptainCustomToolService_Delete_成功(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) tool := createTestCaptainCustomTool(t, db, account.ID) err := svc.Delete(context.Background(), tool.ID) require.NoError(t, err) // 验证已删除(软删除后 GetByID 应返回错误) result, err := svc.Get(context.Background(), tool.ID) assert.Error(t, err) assert.Nil(t, result) } func TestCaptainCustomToolService_Delete_不存在(t *testing.T) { _, _, svc := setupCaptainCustomToolService(t) err := svc.Delete(context.Background(), 9999) // GORM Delete 对不存在的记录不返回错误(匹配 0 行) assert.NoError(t, err) } // ========== List ========== func TestCaptainCustomToolService_List_成功(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 创建多个工具 for i := 0; i < 5; i++ { createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.Slug = fmt.Sprintf("tool-%d", i) t.Title = fmt.Sprintf("工具%d", i) }) } tools, count, err := svc.List(context.Background(), account.ID, 0, 10) require.NoError(t, err) assert.Equal(t, int64(5), count) assert.Len(t, tools, 5) } func TestCaptainCustomToolService_List_分页(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) for i := 0; i < 5; i++ { createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.Slug = fmt.Sprintf("page-tool-%d", i) t.Title = fmt.Sprintf("分页工具%d", i) }) } // 第二页,每页2条,offset=2 tools, count, err := svc.List(context.Background(), account.ID, 2, 2) require.NoError(t, err) assert.Equal(t, int64(5), count) // 总数不变 assert.Len(t, tools, 2) // 只返回2条 } func TestCaptainCustomToolService_List_空列表(t *testing.T) { _, _, svc := setupCaptainCustomToolService(t) tools, count, err := svc.List(context.Background(), 9999, 0, 10) require.NoError(t, err) assert.Equal(t, int64(0), count) assert.Empty(t, tools) } // ========== ExecuteTool ========== func TestCaptainCustomToolService_ExecuteTool_成功GET请求(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 创建测试 HTTP 服务器 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "GET", r.Method) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"result": "success"}) })) defer server.Close() tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeNone }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_成功POST请求(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) assert.Equal(t, "application/json", r.Header.Get("Content-Type")) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{"status": "ok"}) })) defer server.Close() tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "POST" t.AuthType = model.ToolAuthTypeNone }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{ "query": "test", }) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_禁用工具(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 先创建工具,然后通过 service Update 禁用它 tool := createTestCaptainCustomTool(t, db, account.ID) disabled := false _, err := svc.Update(context.Background(), tool.ID, &UpdateCustomToolRequest{Enabled: &disabled}) require.NoError(t, err) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "disabled") } func TestCaptainCustomToolService_ExecuteTool_不存在(t *testing.T) { _, _, svc := setupCaptainCustomToolService(t) result, err := svc.ExecuteTool(context.Background(), 9999, map[string]interface{}{}) assert.Error(t, err) assert.Nil(t, result) assert.Contains(t, err.Error(), "not found") } func TestCaptainCustomToolService_ExecuteTool_Bearer认证(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) expectedToken := "test-bearer-token-123" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { authHeader := r.Header.Get("Authorization") assert.Equal(t, "Bearer "+expectedToken, authHeader) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"auth": "ok"}) })) defer server.Close() authConfig := json.RawMessage(fmt.Sprintf(`{"token":"%s"}`, expectedToken)) tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeBearer t.AuthConfig = authConfig }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_Basic认证(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { username, password, ok := r.BasicAuth() assert.True(t, ok) assert.Equal(t, "testuser", username) assert.Equal(t, "testpass", password) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"auth": "basic_ok"}) })) defer server.Close() authConfig := json.RawMessage(`{"username":"testuser","password":"testpass"}`) tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeBasic t.AuthConfig = authConfig }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_ApiKey认证(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { apiKey := r.Header.Get("X-API-Key") assert.Equal(t, "my-secret-key", apiKey) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"auth": "apikey_ok"}) })) defer server.Close() authConfig := json.RawMessage(`{"key":"X-API-Key","value":"my-secret-key"}`) tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeApiKey t.AuthConfig = authConfig }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_ApiKey自定义Header(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) customHeader := "X-Custom-Token" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { val := r.Header.Get(customHeader) assert.Equal(t, "custom-value-xyz", val) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"auth": "custom_ok"}) })) defer server.Close() authConfig := json.RawMessage(fmt.Sprintf(`{"key":"%s","value":"custom-value-xyz","header":"%s"}`, customHeader, customHeader)) tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeApiKey t.AuthConfig = authConfig }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_带请求模板(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "POST", r.Method) w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]interface{}{"answer": "42"}) })) defer server.Close() tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "POST" t.AuthType = model.ToolAuthTypeNone t.RequestTemplate = `{"query":"{{.keyword}}"}` }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{ "keyword": "golang", }) require.NoError(t, err) assert.True(t, result.Success) } func TestCaptainCustomToolService_ExecuteTool_带响应模板(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"data":{"answer":"42","confidence":"high"}}`)) })) defer server.Close() tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeNone t.ResponseTemplate = "{\"extracted\":\"{{.data.answer}}\",\"conf\":\"{{.data.confidence}}\"}" }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) assert.True(t, result.Success) // 响应模板应被渲染 assert.NotNil(t, result.Data) } func TestCaptainCustomToolService_ExecuteTool_服务端错误(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"error":"internal server error"}`)) })) defer server.Close() tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = server.URL t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeNone }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) require.NoError(t, err) // 不返回 Go error,但 Success=false assert.False(t, result.Success) } // ========== buildRequestBody ========== func TestBuildRequestBody_空模板(t *testing.T) { params := map[string]interface{}{"key": "value"} body, err := buildRequestBody("", params) require.NoError(t, err) var parsed map[string]interface{} require.NoError(t, json.Unmarshal(body, &parsed)) assert.Equal(t, "value", parsed["key"]) } func TestBuildRequestBody_模板渲染(t *testing.T) { params := map[string]interface{}{"keyword": "weather", "location": "Beijing"} body, err := buildRequestBody(`{"query":"{{.keyword}}","loc":"{{.location}}"}`, params) require.NoError(t, err) var parsed map[string]interface{} require.NoError(t, json.Unmarshal(body, &parsed)) assert.Equal(t, "weather", parsed["query"]) assert.Equal(t, "Beijing", parsed["loc"]) } func TestBuildRequestBody_模板解析错误(t *testing.T) { params := map[string]interface{}{"key": "value"} _, err := buildRequestBody(`{{.invalid`, params) assert.Error(t, err) assert.Contains(t, err.Error(), "parse request template") } // ========== parseResponseTemplate ========== func TestParseResponseTemplate_成功(t *testing.T) { rawBody := []byte(`{"data":{"answer":"42"}}`) result, err := parseResponseTemplate(`{"result":"{{.data.answer}}"}`, rawBody) require.NoError(t, err) assert.NotNil(t, result) } func TestParseResponseTemplate_非JSONBody(t *testing.T) { rawBody := []byte(`not json at all`) result, err := parseResponseTemplate(`raw={{.raw}}`, rawBody) require.NoError(t, err) // 当 body 非 JSON 时,会放入 {"raw": "not json at all"},模板渲染后为 "raw=not json at all" assert.Equal(t, "raw=not json at all", result) } func TestParseResponseTemplate_无效模板(t *testing.T) { rawBody := []byte(`{"data":"test"}`) _, err := parseResponseTemplate(`{{.invalid`, rawBody) assert.Error(t, err) assert.Contains(t, err.Error(), "parse response template") } // ========== applyAuth ========== func TestApplyAuth_无认证(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) tool := &model.CaptainCustomTool{AuthType: model.ToolAuthTypeNone} err := applyAuth(req, tool) require.NoError(t, err) // 不应设置任何 Authorization header assert.Empty(t, req.Header.Get("Authorization")) assert.Empty(t, req.Header.Get("X-API-Key")) } func TestApplyAuth_不支持的认证类型(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) tool := &model.CaptainCustomTool{ AuthType: model.ToolAuthType("unknown"), AuthConfig: json.RawMessage(`{}`), } err := applyAuth(req, tool) assert.Error(t, err) assert.Contains(t, err.Error(), "unsupported auth type") } func TestApplyAuth_Basic认证解析失败(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) tool := &model.CaptainCustomTool{ AuthType: model.ToolAuthTypeBasic, AuthConfig: json.RawMessage(`invalid-json`), } err := applyAuth(req, tool) assert.Error(t, err) assert.Contains(t, err.Error(), "parse basic auth config") } func TestApplyAuth_Bearer认证解析失败(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) tool := &model.CaptainCustomTool{ AuthType: model.ToolAuthTypeBearer, AuthConfig: json.RawMessage(`invalid-json`), } err := applyAuth(req, tool) assert.Error(t, err) assert.Contains(t, err.Error(), "parse bearer auth config") } func TestApplyAuth_ApiKey认证解析失败(t *testing.T) { req := httptest.NewRequest("GET", "http://example.com", nil) tool := &model.CaptainCustomTool{ AuthType: model.ToolAuthTypeApiKey, AuthConfig: json.RawMessage(`invalid-json`), } err := applyAuth(req, tool) assert.Error(t, err) assert.Contains(t, err.Error(), "parse api_key auth config") } // ========== ExecuteTool HTTP 网络错误 ========== func TestCaptainCustomToolService_ExecuteTool_网络错误(t *testing.T) { db, _, svc := setupCaptainCustomToolService(t) account := createTestAccount(t, db) // 使用无效 URL 触发网络错误 tool := createTestCaptainCustomTool(t, db, account.ID, func(t *model.CaptainCustomTool) { t.EndpointURL = "http://127.0.0.1:0/invalid" // 端口0无效 t.HTTPMethod = "GET" t.AuthType = model.ToolAuthTypeNone }) result, err := svc.ExecuteTool(context.Background(), tool.ID, map[string]interface{}{}) // 网络错误不返回 Go error,而是返回 Success=false 的结果 require.NoError(t, err) assert.False(t, result.Success) assert.NotEmpty(t, result.Error) }