Files
rill/internal/testutil/testutil.go
T
kevin b683fb293e 增加注册登录与鉴权,并按功能拆分 internal 模块
- 新增 /api/auth/register、/api/auth/login,JWT 签发与 Bearer 鉴权中间件
- notes 需登录,users/user-groups 仅管理员;auth 配置项随版本 1→2 自动补全
- internal/api 仅保留路由装配,拆分为 auth/user/usergroup/note/httpx/testutil
- 同步更新 Swagger 文档与前端注册接口路径
2026-09-20 02:03:06 +08:00

130 lines
3.1 KiB
Go

// Package testutil 提供 HTTP 接口测试的公共环境:临时数据库、路由与凭证。
package testutil
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"rill/internal/api"
"rill/internal/auth"
"rill/internal/config"
"rill/internal/database"
"rill/internal/model"
)
// Env 测试环境:临时数据库、配置、认证器与初始管理员。
type Env struct {
T *testing.T
DB *gorm.DB
Cfg *config.Config
Authn *auth.Authenticator
Admin model.User
}
// Setup 创建临时 SQLite 数据库并执行全部迁移。
func Setup(t *testing.T) *Env {
t.Helper()
gin.SetMode(gin.TestMode)
cfg := &config.Config{
Database: config.DatabaseConfig{
Driver: "sqlite3",
ConnectTimeout: "5s",
SQLite: config.SQLiteConfig{Path: filepath.Join(t.TempDir(), "test.db")},
},
Auth: config.AuthConfig{
Secret: "test-secret",
TokenTTL: "1h",
},
}
db, err := database.Open(cfg)
if err != nil {
t.Fatalf("打开测试数据库失败: %v", err)
}
t.Cleanup(func() {
if err := database.Close(db); err != nil {
t.Errorf("关闭测试数据库失败: %v", err)
}
})
if err := database.Migrate(context.Background(), db); err != nil {
t.Fatalf("执行测试迁移失败: %v", err)
}
env := &Env{T: t, DB: db, Cfg: cfg, Authn: auth.NewAuthenticator(cfg)}
if err := db.Where("username = ?", "admin").First(&env.Admin).Error; err != nil {
t.Fatalf("查询初始管理员失败: %v", err)
}
return env
}
// Router 返回注入了 Authorization 头的路由;token 为空表示匿名访问。
func (e *Env) Router(token string) *gin.Engine {
e.T.Helper()
r := gin.New()
if token != "" {
r.Use(func(c *gin.Context) {
c.Request.Header.Set("Authorization", "Bearer "+token)
c.Next()
})
}
api.RegisterRoutes(r.Group("/api"), e.DB, e.Cfg)
return r
}
// Sign 使用测试密钥为指定用户签发凭证。
func (e *Env) Sign(userID uint) string {
e.T.Helper()
token, _, err := e.Authn.Sign(userID)
if err != nil {
e.T.Fatalf("签发测试凭证失败: %v", err)
}
return token
}
// AdminRouter 返回以初始管理员身份访问的路由。
func (e *Env) AdminRouter() *gin.Engine {
e.T.Helper()
return e.Router(e.Sign(e.Admin.ID))
}
// Call 发送 JSON 请求并返回响应记录器。
func Call(t *testing.T, r http.Handler, method, path string, body any) *httptest.ResponseRecorder {
t.Helper()
var payload []byte
if body != nil {
var err error
payload, err = json.Marshal(body)
if err != nil {
t.Fatalf("序列化请求体失败: %v", err)
}
}
req := httptest.NewRequest(method, path, bytes.NewReader(payload))
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w
}
// DecodeUser 解析用户响应。
func DecodeUser(t *testing.T, w *httptest.ResponseRecorder) model.User {
t.Helper()
var user model.User
if err := json.Unmarshal(w.Body.Bytes(), &user); err != nil {
t.Fatalf("解析用户响应失败: %v, body=%s", err, w.Body.String())
}
return user
}