feat(captain): validate custom tools

This commit is contained in:
2026-06-07 16:20:39 +08:00
parent dd7bcf4118
commit 8f9adecd04
5 changed files with 213 additions and 12 deletions
@@ -246,6 +246,67 @@ func (s *CaptainCustomToolCRUDTestSuite) TestCreate_超过每账户15个工具
assert.Equal(s.T(), "You can create a maximum of 15 custom tools per account", resp["error"])
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_无效枚举返回RecordInvalid形态422() {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Invalid enums",
"endpoint_url": "https://example.com/invalid",
"http_method": "DELETE",
"auth_type": "oauth",
},
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Contains(s.T(), resp["message"], "Http method is not included in the list")
assert.Contains(s.T(), resp["message"], "Auth type is not included in the list")
assert.ElementsMatch(s.T(), []interface{}{"http_method", "auth_type"}, resp["attributes"].([]interface{}))
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_无效ParamSchema返回RecordInvalid形态422() {
body := map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Invalid schema",
"endpoint_url": "https://example.com/schema",
"param_schema": []map[string]interface{}{{
"name": "order_id",
"type": "string",
"extra": "not allowed",
}},
},
}
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", body)
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Contains(s.T(), resp["message"], "Description is required")
assert.Contains(s.T(), resp["message"], "Extra is not permitted")
assert.ElementsMatch(s.T(), []interface{}{"description", "extra"}, resp["attributes"].([]interface{}))
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_重复显式Slug返回422() {
_ = s.createToolAndGetID("First duplicate", "explicit-duplicate", "https://example.com/first")
w := s.makeRequest("POST", s.accountPath()+"/captain/custom_tools/", map[string]interface{}{
"custom_tool": map[string]interface{}{
"title": "Second duplicate",
"slug": "explicit-duplicate",
"endpoint_url": "https://example.com/second",
},
})
assert.Equal(s.T(), http.StatusUnprocessableEntity, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(s.T(), "Slug has already been taken", resp["message"])
assert.ElementsMatch(s.T(), []interface{}{"slug"}, resp["attributes"].([]interface{}))
}
func (s *CaptainCustomToolCRUDTestSuite) TestCreate_无效JSON返回400() {
// ShouldBindJSON 在 JSON 解析失败时返回 400
w := httptest.NewRecorder()
@@ -47,6 +47,9 @@ func (h *CaptainCustomToolHandler) Create(c *gin.Context) {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": service.ErrCaptainCustomToolLimitExceeded.Error()})
return
}
if renderCaptainCustomToolValidationError(c, err) {
return
}
response.AbortWithStatusError(c, http.StatusInternalServerError, response.ErrInternal, "failed to create custom tool")
return
}
@@ -107,6 +110,9 @@ func (h *CaptainCustomToolHandler) Update(c *gin.Context) {
tool, err := h.svc.UpdateByAccount(c.Request.Context(), accountID, id, &req)
if err != nil {
applogger.L().Errorf("Update captain custom tool: %v", err)
if renderCaptainCustomToolValidationError(c, err) {
return
}
handleServiceError(c, err)
return
}
@@ -227,6 +233,18 @@ func (h *CaptainCustomToolHandler) ensureCustomToolsEnabled(c *gin.Context, acco
return false
}
func renderCaptainCustomToolValidationError(c *gin.Context, err error) bool {
var validationErr *service.CaptainCustomToolValidationError
if !errors.As(err, &validationErr) {
return false
}
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": validationErr.Message,
"attributes": validationErr.Attributes,
})
return true
}
func captainCustomToolPayload(tool *model.CaptainCustomTool) gin.H {
return gin.H{
"id": tool.ID,
@@ -187,7 +187,7 @@ func TestCaptainCustomToolHandler_ChatwootToolPayloadsAndScope(t *testing.T) {
"endpoint_url": "https://example.com/orders",
"http_method": "POST",
"auth_type": "none",
"param_schema": []map[string]any{{"name": "order_id", "type": "string", "required": true}},
"param_schema": []map[string]any{{"name": "order_id", "type": "string", "description": "Order ID", "required": true}},
"request_template": "{\"id\":\"{{.order_id}}\"}",
}}
w := captainResourceJSONRequest(t, router, http.MethodPost, basePath+"/", body)