Files
2026-07-14 10:24:10 +08:00

161 lines
5.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"strings"
"testing"
"time"
"github.com/EthanCodeCraft/xlgo-core/config"
"github.com/EthanCodeCraft/xlgo-core/database"
"github.com/EthanCodeCraft/xlgo-core/jwt"
"github.com/EthanCodeCraft/xlgo-core/validation"
"github.com/alicebob/miniredis/v2"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/redis/go-redis/v9"
"gorm.io/gorm"
)
func setupExampleRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
t.Helper()
database.RegisterDialect(database.DialectSpec{
Name: "sqlite_full_example_test",
Dialector: func(dsn string) gorm.Dialector { return sqlite.Open(dsn) },
DSN: func(c *config.DatabaseConfig) string {
return c.CustomDSN
},
})
cfg := &config.Config{
Database: config.DatabaseConfig{
Driver: "sqlite_full_example_test",
CustomDSN: filepath.Join(t.TempDir(), "full-example.db"),
},
JWT: config.JWTConfig{
Secret: "test-secret-for-full-example-at-least-32-bytes",
Expire: time.Hour,
RefreshExpire: 24 * time.Hour,
Issuer: "xlgo",
Algorithm: "HS256",
},
}
if err := config.Set(cfg); err != nil {
t.Fatalf("设置测试配置失败: %v", err)
}
mgr := database.NewManager(cfg)
if err := mgr.InitDB(context.Background(), cfg); err != nil {
t.Fatalf("初始化测试数据库失败: %v", err)
}
db := mgr.Master()
if err := db.AutoMigrate(&User{}); err != nil {
t.Fatalf("迁移示例用户表失败: %v", err)
}
prev := database.SwapDefaultManager(mgr)
t.Cleanup(func() {
current := database.SwapDefaultManager(prev)
if current != nil && current != prev {
_ = current.Close()
}
})
// JWT 黑名单走 RedisParseToken 默认 fail-closed,无 Redis 会拒绝所有 token)。
// 测试用 miniredis 注入,避免依赖外部 Redis。
mr := miniredis.RunT(t)
redisClient := redis.NewClient(&redis.Options{Addr: mr.Addr()})
t.Cleanup(func() { _ = redisClient.Close() })
prevJWTMgr := jwt.GetDefaultJWT()
jwt.SetDefaultJWTManager(jwt.NewJWTManagerWithRedis(redisClient))
t.Cleanup(func() { jwt.SetDefaultJWTManager(prevJWTMgr) })
gin.SetMode(gin.TestMode)
r := gin.New()
registerRoutes(r.Group(""))
if err := ensureExampleUser(context.Background()); err != nil {
t.Fatalf("初始化示例用户失败: %v", err)
}
return r, db
}
func postJSON(t *testing.T, r http.Handler, path string, body string, token string) (int, map[string]any) {
t.Helper()
req := httptest.NewRequest(http.MethodPost, path, bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
var payload map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
t.Fatalf("解析响应 JSON 失败: %v, body=%s", err, rec.Body.String())
}
return rec.Code, payload
}
func tokenFromResponse(t *testing.T, payload map[string]any) string {
t.Helper()
data, ok := payload["data"].(map[string]any)
if !ok {
t.Fatalf("响应缺少 data 对象: %#v", payload)
}
token, ok := data["token"].(string)
if !ok || strings.TrimSpace(token) == "" {
t.Fatalf("响应缺少 token: %#v", payload)
}
return token
}
func TestFullExampleLoginAndPasswordFlow(t *testing.T) {
r, db := setupExampleRouter(t)
var seeded User
if err := db.Where("username = ?", "alice").First(&seeded).Error; err != nil {
t.Fatalf("示例用户 alice 应在启动时初始化: %v", err)
}
if seeded.Password == "" || seeded.Password == "secret" {
t.Fatal("示例用户密码必须保存为 bcrypt 哈希,不能保存明文")
}
if !validation.CheckPassword(seeded.Password, "secret") {
t.Fatal("示例用户 alice/secret 应能通过密码校验")
}
status, wrong := postJSON(t, r, "/api/v1/login", `{"username":"alice","password":"wrong"}`, "")
if status != http.StatusOK {
t.Fatalf("业务失败模式下 HTTP 状态应为 200got %d", status)
}
if code, _ := wrong["code"].(float64); code == 0 {
t.Fatalf("错误密码应返回业务失败: %#v", wrong)
}
_, loginPayload := postJSON(t, r, "/api/v1/login", `{"username":"alice","password":"secret"}`, "")
token := tokenFromResponse(t, loginPayload)
_, createPayload := postJSON(t, r, "/api/v1/users", `{"username":"bob","password":"s3cret"}`, token)
if code, _ := createPayload["code"].(float64); code != 0 {
t.Fatalf("带 token 创建用户应成功: %#v", createPayload)
}
var bob User
if err := db.Where("username = ?", "bob").First(&bob).Error; err != nil {
t.Fatalf("创建后的用户应写入数据库: %v", err)
}
if bob.Password == "" || bob.Password == "s3cret" {
t.Fatal("创建用户时必须保存 bcrypt 哈希,不能保存明文")
}
if !validation.CheckPassword(bob.Password, "s3cret") {
t.Fatal("创建用户的密码哈希应能通过校验")
}
_, bobLogin := postJSON(t, r, "/api/v1/login", `{"username":"bob","password":"s3cret"}`, "")
_ = tokenFromResponse(t, bobLogin)
}