Files
rill/internal/api/notes_test.go
T
kevin 62f02f8a39 增加用户与用户组模块及初始管理员
- users/user_groups 模型与 CRUD 接口,组成员关系手动维护(规避 GORM 零值主键问题)
- 迁移 v2~v4:用户组、用户表、初始 admin 用户
- 初始密码随机生成,仅终端打印一次并写入 data/admin_password.txt
2026-09-19 17:10:54 +08:00

183 lines
5.0 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"path/filepath"
"testing"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"rill/internal/config"
"rill/internal/database"
"rill/internal/model"
)
func setupRouter(t *testing.T) *gin.Engine {
t.Helper()
r, _ := setupRouterWithDB(t)
return r
}
func setupRouterWithDB(t *testing.T) (*gin.Engine, *gorm.DB) {
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")},
},
}
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)
}
r := gin.New()
RegisterRoutes(r.Group("/api"), db)
return r, db
}
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
}
func decodeNote(t *testing.T, w *httptest.ResponseRecorder) model.Note {
t.Helper()
var note model.Note
if err := json.Unmarshal(w.Body.Bytes(), &note); err != nil {
t.Fatalf("解析响应失败: %v, body=%s", err, w.Body.String())
}
return note
}
func TestHealth(t *testing.T) {
r := setupRouter(t)
w := call(t, r, http.MethodGet, "/api/health", nil)
if w.Code != http.StatusOK {
t.Fatalf("状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusOK, w.Body.String())
}
var resp map[string]string
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
t.Fatalf("解析响应失败: %v", err)
}
if resp["status"] != "ok" {
t.Errorf("status = %q, 期望 ok", resp["status"])
}
}
func TestNoteCRUD(t *testing.T) {
r := setupRouter(t)
w := call(t, r, http.MethodPost, "/api/notes", map[string]string{"title": "第一条", "content": "内容"})
if w.Code != http.StatusCreated {
t.Fatalf("创建状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusCreated, w.Body.String())
}
created := decodeNote(t, w)
if created.ID == 0 || created.Title != "第一条" || created.Content != "内容" {
t.Fatalf("创建结果异常: %+v", created)
}
detailPath := fmt.Sprintf("/api/notes/%d", created.ID)
w = call(t, r, http.MethodGet, detailPath, nil)
if w.Code != http.StatusOK {
t.Fatalf("详情状态码 = %d, 期望 %d", w.Code, http.StatusOK)
}
if got := decodeNote(t, w); got.ID != created.ID {
t.Errorf("详情 ID = %d, 期望 %d", got.ID, created.ID)
}
w = call(t, r, http.MethodPut, detailPath, map[string]string{"title": "已更新", "content": "新内容"})
if w.Code != http.StatusOK {
t.Fatalf("更新状态码 = %d, 期望 %d, body=%s", w.Code, http.StatusOK, w.Body.String())
}
updated := decodeNote(t, w)
if updated.Title != "已更新" || updated.Content != "新内容" {
t.Errorf("更新结果异常: %+v", updated)
}
w = call(t, r, http.MethodGet, "/api/notes?page=1&page_size=10", nil)
if w.Code != http.StatusOK {
t.Fatalf("列表状态码 = %d, 期望 %d", w.Code, http.StatusOK)
}
var list struct {
Items []model.Note `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := json.Unmarshal(w.Body.Bytes(), &list); err != nil {
t.Fatalf("解析列表响应失败: %v", err)
}
if list.Total != 1 || len(list.Items) != 1 {
t.Fatalf("列表结果异常: total=%d, items=%d", list.Total, len(list.Items))
}
if list.Items[0].Title != "已更新" {
t.Errorf("列表项标题 = %q, 期望 已更新", list.Items[0].Title)
}
w = call(t, r, http.MethodDelete, detailPath, nil)
if w.Code != http.StatusNoContent {
t.Fatalf("删除状态码 = %d, 期望 %d", w.Code, http.StatusNoContent)
}
w = call(t, r, http.MethodGet, detailPath, nil)
if w.Code != http.StatusNotFound {
t.Errorf("删除后详情状态码 = %d, 期望 %d", w.Code, http.StatusNotFound)
}
}
func TestNoteValidation(t *testing.T) {
r := setupRouter(t)
w := call(t, r, http.MethodPost, "/api/notes", map[string]string{"content": "缺少标题"})
if w.Code != http.StatusBadRequest {
t.Errorf("缺少标题状态码 = %d, 期望 %d", w.Code, http.StatusBadRequest)
}
w = call(t, r, http.MethodGet, "/api/notes/abc", nil)
if w.Code != http.StatusBadRequest {
t.Errorf("非法 id 状态码 = %d, 期望 %d", w.Code, http.StatusBadRequest)
}
w = call(t, r, http.MethodGet, "/api/notes/9999", nil)
if w.Code != http.StatusNotFound {
t.Errorf("不存在记录状态码 = %d, 期望 %d", w.Code, http.StatusNotFound)
}
}