115 lines
3.8 KiB
Go
115 lines
3.8 KiB
Go
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("重置凭证未在事务中消费")
|
|
}
|
|
}
|