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"}, } }