package main import ( "context" "encoding/json" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" businessbiz "kra/internal/biz/business" businessdata "kra/internal/data/business" systemdata "kra/internal/data/system" "kra/internal/server/handler" "kra/internal/server/router" businessservice "kra/internal/service/business" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" ) type resetHTTPSMS struct { code string calls int } func (s *resetHTTPSMS) SendCode(_ context.Context, _ string, code string) error { s.code = code s.calls++ return nil } func TestPasswordResetAnonymousHTTPFlow(t *testing.T) { db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "reset.db")), &gorm.Config{}) if err != nil { t.Fatal(err) } sqlDB, err := db.DB() if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = sqlDB.Close() }) for _, step := range append(systemdata.Migrations(), businessdata.Migrations()...) { if err := step.Migrate(db); err != nil { t.Fatal(err) } } before := time.Now().Add(-time.Hour) if err := db.Table("sys_users").Create(map[string]any{"id": 7, "uuid": "reset-test", "username": "recover", "phone": "13800138000", "enable": 1, "business_status": 0, "password": "old-hash", "password_updated_at": before}).Error; err != nil { t.Fatal(err) } sms := &resetHTTPSMS{} service := businessservice.NewPasswordResetService(businessbiz.NewPasswordResetUsecase(businessdata.NewPasswordResetRepo(businessHTTPData{db}), sms)) engine := gin.New() router.RegisterBusinessPassword(engine.Group(""), handler.NewBusinessPassword(service)) server := httptest.NewServer(engine) defer server.Close() call := func(action string, payload map[string]any, expected int) map[string]any { t.Helper() encoded, _ := json.Marshal(payload) response, err := http.Post(server.URL+"/sw/auth/forgot-password/"+action, "application/json", strings.NewReader(string(encoded))) if err != nil { t.Fatal(err) } defer response.Body.Close() var body map[string]any if err := json.NewDecoder(response.Body).Decode(&body); err != nil { t.Fatal(err) } if response.StatusCode != 200 || body["code"] != float64(expected) { t.Fatalf("%s 结果不正确: %+v", action, body) } data, _ := body["data"].(map[string]any) return data } phone := call("check", map[string]any{"username": "recover"}, 0) if phone["maskedPhone"] != "138****8000" { t.Fatal("接口手机号未脱敏") } call("send-code", map[string]any{"username": "recover"}, 0) call("send-code", map[string]any{"username": "recover"}, 7) if sms.calls != 1 { t.Fatal("HTTP 发码限流无效") } wrong := "000000" if sms.code == wrong { wrong = "111111" } call("verify-code", map[string]any{"username": "recover", "smsCode": wrong}, 7) verified := call("verify-code", map[string]any{"username": "recover", "smsCode": sms.code}, 0) token, ok := verified["resetToken"].(string) if !ok || len(token) != 64 { t.Fatal("未返回一次性重置凭证") } call("verify-code", map[string]any{"username": "recover", "smsCode": sms.code}, 7) call("reset", map[string]any{"resetToken": token, "newPassword": "abc123", "confirmPassword": "abc123"}, 0) var user struct { Password string PasswordUpdatedAt time.Time } if err := db.Table("sys_users").Where("id = ?", 7).Take(&user).Error; err != nil { t.Fatal(err) } if bcrypt.CompareHashAndPassword([]byte(user.Password), []byte("abc123")) != nil || !user.PasswordUpdatedAt.After(before) { t.Fatal("新密码或会话失效版本未保存") } call("reset", map[string]any{"resetToken": token, "newPassword": "abc124", "confirmPassword": "abc124"}, 7) var count int64 if err := db.Table("sys_business_password_resets").Count(&count).Error; err != nil || count != 0 { t.Fatal("重置凭证未在事务中消费") } }