- 新增 /api/auth/register、/api/auth/login,JWT 签发与 Bearer 鉴权中间件 - notes 需登录,users/user-groups 仅管理员;auth 配置项随版本 1→2 自动补全 - internal/api 仅保留路由装配,拆分为 auth/user/usergroup/note/httpx/testutil - 同步更新 Swagger 文档与前端注册接口路径
130 lines
3.1 KiB
Go
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
|
|
}
|