fix: 修复 AI 服务商配置新增失败 (#222)

This commit is contained in:
chengma
2026-08-14 11:04:51 +08:00
parent 57fc8a3392
commit 01469f74b2
4 changed files with 216 additions and 1 deletions
+121
View File
@@ -0,0 +1,121 @@
package web
import (
"database/sql"
"html/template"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"github.com/gin-gonic/gin"
"cmautobuy/admin/internal/testutil"
"cmautobuy/admin/model"
"cmautobuy/admin/repository"
"cmautobuy/admin/service"
)
func TestRedirectAIConfig_区分成功和校验失败(t *testing.T) {
gin.SetMode(gin.TestMode)
tests := []struct {
name, message, errorMessage, wantKey, wantValue string
}{
{name: "成功", message: "配置已保存", wantKey: "msg", wantValue: "配置已保存"},
{name: "校验失败", errorMessage: "Base URL 无效", wantKey: "error", wantValue: "Base URL 无效"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
recorder := httptest.NewRecorder()
context, _ := gin.CreateTestContext(recorder)
context.Request = httptest.NewRequest(http.MethodPost, "/settings/ai/save", nil)
redirectAIConfig(context, test.message, test.errorMessage)
if context.Writer.Status() != http.StatusSeeOther {
t.Fatalf("状态码=%d,期望 303", context.Writer.Status())
}
location, err := url.Parse(recorder.Header().Get("Location"))
if err != nil || location.Path != "/settings/ai" {
t.Fatalf("Location=%q err=%v", recorder.Header().Get("Location"), err)
}
if got := location.Query().Get(test.wantKey); got != test.wantValue {
t.Fatalf("%s=%q,期望 %q", test.wantKey, got, test.wantValue)
}
otherKey := "msg"
if test.wantKey == "msg" {
otherKey = "error"
}
if location.Query().Has(otherKey) {
t.Fatalf("成功和错误参数不能混用: %s", location.String())
}
})
}
}
func TestAIConfigSave_校验失败使用Error跳转(t *testing.T) {
context, recorder, _ := aiConfigSaveContext(t, url.Values{"confidence_threshold": {"not-a-number"}})
context.Set(currentUserKey, &model.User{Role: model.RoleAdmin, Status: model.UserActive})
(&Handler{}).AIConfigSave(context)
location, err := url.Parse(recorder.Header().Get("Location"))
if context.Writer.Status() != http.StatusSeeOther || err != nil || location.Query().Get("error") == "" || location.Query().Has("msg") {
t.Fatalf("校验失败跳转错误: code=%d location=%q err=%v", context.Writer.Status(), recorder.Header().Get("Location"), err)
}
}
func TestAIConfigSave_数据库异常返回500而非成功跳转(t *testing.T) {
db, err := sql.Open("mysql", "")
if err != nil {
t.Fatal(err)
}
if err := db.Close(); err != nil {
t.Fatal(err)
}
context, recorder, engine := aiConfigSaveContext(t, validAIConfigForm())
context.Set(currentUserKey, &model.User{UserID: "USR-1", Role: model.RoleAdmin, Status: model.UserActive})
engine.SetHTMLTemplate(template.Must(template.New("root").Parse(
`{{define "partials/error"}}{{.Message}}{{end}}`,
)))
(&Handler{db: db, aiPolicy: service.NewAIEndpointPolicy(nil)}).AIConfigSave(context)
if recorder.Code != http.StatusInternalServerError || recorder.Header().Get("Location") != "" {
t.Fatalf("数据库异常不能伪装成 303 成功: code=%d location=%q", recorder.Code, recorder.Header().Get("Location"))
}
}
func TestAIConfigSave_真实MySQL新增后列表可见并使用Msg跳转(t *testing.T) {
db := testutil.OpenMySQL(t)
actor := model.User{UserID: "USR-WEB-AI", Username: "web-ai-admin", PasswordHash: "test-hash",
Role: model.RoleAdmin, Status: model.UserActive, PasswordChangedAt: model.NowISO(),
CreatedAt: model.NowISO(), UpdatedAt: model.NowISO()}
if err := repository.CreateUser(db, actor); err != nil {
t.Fatal(err)
}
context, recorder, _ := aiConfigSaveContext(t, validAIConfigForm())
context.Set(currentUserKey, &actor)
(&Handler{db: db, aiPolicy: service.NewAIEndpointPolicy(nil)}).AIConfigSave(context)
location, err := url.Parse(recorder.Header().Get("Location"))
if context.Writer.Status() != http.StatusSeeOther || err != nil || location.Query().Get("msg") == "" || location.Query().Has("error") {
t.Fatalf("成功跳转错误: code=%d location=%q err=%v", context.Writer.Status(), recorder.Header().Get("Location"), err)
}
items, err := repository.ListAIProviders(db)
if err != nil || len(items) != 1 || items[0].Name != "测试主线路" {
t.Fatalf("跳转后应能查询新增配置: items=%+v err=%v", items, err)
}
}
func aiConfigSaveContext(t *testing.T, values url.Values) (*gin.Context, *httptest.ResponseRecorder, *gin.Engine) {
t.Helper()
gin.SetMode(gin.TestMode)
recorder := httptest.NewRecorder()
context, engine := gin.CreateTestContext(recorder)
request := httptest.NewRequest(http.MethodPost, "/settings/ai/save", strings.NewReader(values.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
context.Request = request
return context, recorder, engine
}
func validAIConfigForm() url.Values {
return url.Values{
"name": {"测试主线路"}, "base_url": {"https://api.example.com/v1"}, "model": {"model-1"},
"timeout_seconds": {"30"}, "max_concurrency": {"2"}, "confidence_threshold": {"85"},
}
}