- 使用 Squirrel 与 sqlx 实现分类查询 - 集中组装依赖并向 handler 注入 model 接口 - 显式转换分类响应 DTO,统一空列表输出 - 补充路由与 handler 测试,同步架构文档 BREAKING CHANGE: 将 GET /hello 替换为 GET /test,响应由问候字符串改为分类列表
129 lines
3.6 KiB
Go
129 lines
3.6 KiB
Go
package handler_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"lyra/internal/handler"
|
|
"lyra/internal/model"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type fakeCategoryModel struct {
|
|
categories []*model.Category
|
|
err error
|
|
calls int
|
|
gotCtx context.Context
|
|
}
|
|
|
|
func (f *fakeCategoryModel) FindAll(ctx context.Context) ([]*model.Category, error) {
|
|
f.calls++
|
|
f.gotCtx = ctx
|
|
return f.categories, f.err
|
|
}
|
|
|
|
func serveHello(t *testing.T, fake *fakeCategoryModel, req *http.Request) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
engine := gin.New()
|
|
engine.GET("/test", handler.NewTestHandler(fake).Hello())
|
|
recorder := httptest.NewRecorder()
|
|
engine.ServeHTTP(recorder, req)
|
|
return recorder
|
|
}
|
|
|
|
func newHelloRequest() *http.Request {
|
|
return httptest.NewRequest(http.MethodGet, "/test", nil)
|
|
}
|
|
|
|
func TestHelloMapsCategoriesToResponse(t *testing.T) {
|
|
fake := &fakeCategoryModel{categories: []*model.Category{
|
|
{ID: 1, Slug: "go-basics", Title: "Go 基础", Sort: 1},
|
|
{ID: 2, Slug: "sql-practice", Title: "SQL 练习", Sort: 2},
|
|
}}
|
|
|
|
recorder := serveHello(t, fake, newHelloRequest())
|
|
|
|
if fake.calls != 1 {
|
|
t.Fatalf("模型调用次数 = %d, want 1", fake.calls)
|
|
}
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
|
}
|
|
want := `{"success":true,"data":[{"id":1,"slug":"go-basics","title":"Go 基础","sort":1},{"id":2,"slug":"sql-practice","title":"SQL 练习","sort":2}]}`
|
|
if got := recorder.Body.String(); got != want {
|
|
t.Fatalf("body = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestHelloReturnsEmptyArrayForNoCategories(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
categories []*model.Category
|
|
}{
|
|
{name: "空切片", categories: []*model.Category{}},
|
|
{name: "nil结果", categories: nil},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
fake := &fakeCategoryModel{categories: tt.categories}
|
|
|
|
recorder := serveHello(t, fake, newHelloRequest())
|
|
|
|
if fake.calls != 1 {
|
|
t.Fatalf("模型调用次数 = %d, want 1", fake.calls)
|
|
}
|
|
if recorder.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusOK)
|
|
}
|
|
want := `{"success":true,"data":[]}`
|
|
if got := recorder.Body.String(); got != want {
|
|
t.Fatalf("body = %q, want %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHelloReturnsInternalErrorOnModelFailure(t *testing.T) {
|
|
modelErr := errors.New("数据库连接被拒绝: 10.0.0.8:5432")
|
|
fake := &fakeCategoryModel{err: modelErr}
|
|
|
|
recorder := serveHello(t, fake, newHelloRequest())
|
|
|
|
if fake.calls != 1 {
|
|
t.Fatalf("模型调用次数 = %d, want 1", fake.calls)
|
|
}
|
|
if recorder.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status = %d, want %d", recorder.Code, http.StatusInternalServerError)
|
|
}
|
|
want := `{"success":false,"error":{"code":10002,"message":"获取分类列表失败"}}`
|
|
if got := recorder.Body.String(); got != want {
|
|
t.Fatalf("body = %q, want %q", got, want)
|
|
}
|
|
if strings.Contains(recorder.Body.String(), modelErr.Error()) {
|
|
t.Fatalf("响应泄漏了内部错误信息: %q", recorder.Body.String())
|
|
}
|
|
}
|
|
|
|
type contextKey struct{}
|
|
|
|
func TestHelloPassesRequestContextToModel(t *testing.T) {
|
|
fake := &fakeCategoryModel{categories: []*model.Category{}}
|
|
reqCtx := context.WithValue(context.Background(), contextKey{}, "req-42")
|
|
req := newHelloRequest().WithContext(reqCtx)
|
|
|
|
serveHello(t, fake, req)
|
|
|
|
if fake.gotCtx != reqCtx {
|
|
t.Fatalf("模型收到非请求 context: %v", fake.gotCtx)
|
|
}
|
|
if got, _ := fake.gotCtx.Value(contextKey{}).(string); got != "req-42" {
|
|
t.Fatalf("context 值 = %q, want %q", got, "req-42")
|
|
}
|
|
}
|