package test import ( "bytes" "encoding/json" "errors" "io" "mime/multipart" "net/http" "net/http/httptest" "sync" "testing" "github.com/gin-gonic/gin" ) // SetupRouter 创建不带框架默认中间件的测试路由。 // // 需要 RequestID、Recover、超时等框架中间件的集成测试,应直接构造 xlgo.App // 或手动 Use 对应中间件。 func SetupRouter() *gin.Engine { gin.SetMode(gin.TestMode) return gin.New() } // Request 是测试请求构造器。 type Request struct { router *gin.Engine method string path string body any headers map[string]string } // NewRequest 创建测试请求。 func NewRequest(router *gin.Engine, method, path string) *Request { return &Request{ router: router, method: method, path: path, headers: make(map[string]string), } } // GET 创建 GET 请求。 func GET(router *gin.Engine, path string) *Request { return NewRequest(router, http.MethodGet, path) } // POST 创建 POST 请求。 func POST(router *gin.Engine, path string) *Request { return NewRequest(router, http.MethodPost, path) } // PUT 创建 PUT 请求。 func PUT(router *gin.Engine, path string) *Request { return NewRequest(router, http.MethodPut, path) } // DELETE 创建 DELETE 请求。 func DELETE(router *gin.Engine, path string) *Request { return NewRequest(router, http.MethodDelete, path) } // PATCH 创建 PATCH 请求。 func PATCH(router *gin.Engine, path string) *Request { return NewRequest(router, http.MethodPatch, path) } // WithBody 设置请求体。 func (r *Request) WithBody(body any) *Request { r.body = body return r } // WithJSON 设置 JSON 请求体并写入 Content-Type。 func (r *Request) WithJSON(body any) *Request { r.body = body r.headers["Content-Type"] = "application/json" return r } // WithHeader 设置请求头。 func (r *Request) WithHeader(key, value string) *Request { r.headers[key] = value return r } // WithToken 设置 Authorization Bearer token。 func (r *Request) WithToken(token string) *Request { r.headers["Authorization"] = "Bearer " + token return r } // Execute 执行请求并返回带断言方法的响应包装。 func (r *Request) Execute() *Response { return &Response{ResponseRecorder: r.ExecuteRecorder()} } // ExecuteRecorder 执行请求并返回原始 httptest.ResponseRecorder。 func (r *Request) ExecuteRecorder() *httptest.ResponseRecorder { var bodyReader *bytes.Reader if r.body != nil { bodyBytes, _ := json.Marshal(r.body) bodyReader = bytes.NewReader(bodyBytes) } else { bodyReader = bytes.NewReader(nil) } req := httptest.NewRequest(r.method, r.path, bodyReader) for key, value := range r.headers { req.Header.Set(key, value) } recorder := httptest.NewRecorder() r.router.ServeHTTP(recorder, req) return recorder } // Response 是测试响应辅助包装。 type Response struct { *httptest.ResponseRecorder } // ParseJSON 解析 JSON 响应。 func (r *Response) ParseJSON(v any) error { return json.Unmarshal(r.Body.Bytes(), v) } // AssertStatus 断言 HTTP 状态码。 func (r *Response) AssertStatus(t *testing.T, expected int) { t.Helper() if r.Code != expected { t.Errorf("状态码错误: 期望 %d, 实际 %d", expected, r.Code) } } // AssertOK 断言状态码为 200。 func (r *Response) AssertOK(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusOK) } // AssertCreated 断言状态码为 201。 func (r *Response) AssertCreated(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusCreated) } // AssertBadRequest 断言状态码为 400。 func (r *Response) AssertBadRequest(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusBadRequest) } // AssertUnauthorized 断言状态码为 401。 func (r *Response) AssertUnauthorized(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusUnauthorized) } // AssertForbidden 断言状态码为 403。 func (r *Response) AssertForbidden(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusForbidden) } // AssertNotFound 断言状态码为 404。 func (r *Response) AssertNotFound(t *testing.T) { t.Helper() r.AssertStatus(t, http.StatusNotFound) } // AssertJSONContains 断言 JSON 响应包含指定字段和值,支持 data.user.id 形式的嵌套键。 func (r *Response) AssertJSONContains(t *testing.T, key string, expected any) { t.Helper() var result map[string]any if err := r.ParseJSON(&result); err != nil { t.Errorf("解析 JSON 失败: %v", err) return } keys := splitKey(key) current := result for i, k := range keys { if i == len(keys)-1 { if current[k] != expected { t.Errorf("JSON 字段 %s 错误: 期望 %v, 实际 %v", key, expected, current[k]) } return } next, ok := current[k].(map[string]any) if !ok { t.Errorf("JSON 字段 %s 不是对象", k) return } current = next } } func splitKey(key string) []string { var keys []string start := 0 for i := 0; i < len(key); i++ { if key[i] == '.' { keys = append(keys, key[start:i]) start = i + 1 } } keys = append(keys, key[start:]) return keys } // MockDB 是并发安全的简单内存数据库 mock。 type MockDB struct { mu sync.RWMutex data map[any]any } // NewMockDB 创建模拟数据库。 func NewMockDB() *MockDB { return &MockDB{data: make(map[any]any)} } // Set 设置数据。 func (m *MockDB) Set(key, value any) { m.mu.Lock() defer m.mu.Unlock() m.data[key] = value } // Get 获取数据。 func (m *MockDB) Get(key any) (any, bool) { m.mu.RLock() defer m.mu.RUnlock() v, ok := m.data[key] return v, ok } // Delete 删除数据。 func (m *MockDB) Delete(key any) { m.mu.Lock() defer m.mu.Unlock() delete(m.data, key) } // Clear 清空数据。 func (m *MockDB) Clear() { m.mu.Lock() defer m.mu.Unlock() m.data = make(map[any]any) } // MockCache 是并发安全的简单内存缓存 mock。 type MockCache struct { mu sync.RWMutex data map[string][]byte } // NewMockCache 创建模拟缓存。 func NewMockCache() *MockCache { return &MockCache{data: make(map[string][]byte)} } // Set 设置缓存,内部会复制 value,避免调用方后续修改污染 mock。 func (m *MockCache) Set(key string, value []byte) { m.mu.Lock() defer m.mu.Unlock() m.data[key] = append([]byte(nil), value...) } // Get 获取缓存,返回值会复制一份,避免调用方修改内部状态。 func (m *MockCache) Get(key string) ([]byte, bool) { m.mu.RLock() defer m.mu.RUnlock() v, ok := m.data[key] return append([]byte(nil), v...), ok } // Delete 删除缓存。 func (m *MockCache) Delete(key string) { m.mu.Lock() defer m.mu.Unlock() delete(m.data, key) } // Exists 检查缓存是否存在。 func (m *MockCache) Exists(key string) bool { m.mu.RLock() defer m.mu.RUnlock() _, ok := m.data[key] return ok } // Clear 清空缓存。 func (m *MockCache) Clear() { m.mu.Lock() defer m.mu.Unlock() m.data = make(map[string][]byte) } // ErrMockStorageTooLarge 表示 mock storage 收到超过内存保护上限的内容。 var ErrMockStorageTooLarge = errors.New("mock storage: 文件超过大小限制") const mockStorageMaxBytes int64 = 32 << 20 // MockStorage 是并发安全的简单内存存储 mock。 type MockStorage struct { mu sync.RWMutex files map[string][]byte urls map[string]string } // NewMockStorage 创建模拟存储。 func NewMockStorage() *MockStorage { return &MockStorage{ files: make(map[string][]byte), urls: make(map[string]string), } } // Upload 模拟上传,签名对齐 storage.Upload。 func (m *MockStorage) Upload(file *multipart.FileHeader, subdir string) (string, error) { if file == nil { return "", errors.New("mock storage: 文件不能为空") } if file.Size > mockStorageMaxBytes { return "", ErrMockStorageTooLarge } data, err := file.Open() if err != nil { return "", err } b, err := io.ReadAll(io.LimitReader(data, mockStorageMaxBytes+1)) if cerr := data.Close(); cerr != nil { err = errors.Join(err, cerr) } if err != nil { return "", err } if int64(len(b)) > mockStorageMaxBytes { return "", ErrMockStorageTooLarge } path := "/mock/" + subdir + "/" + file.Filename m.mu.Lock() defer m.mu.Unlock() m.files[path] = b return path, nil } // UploadFromBytes 模拟字节上传,签名对齐 storage.UploadFromBytes。 func (m *MockStorage) UploadFromBytes(data []byte, filename, subdir string) (string, error) { if int64(len(data)) > mockStorageMaxBytes { return "", ErrMockStorageTooLarge } path := "/mock/" + subdir + "/" + filename m.mu.Lock() defer m.mu.Unlock() m.files[path] = append([]byte(nil), data...) return path, nil } // GetURL 获取 mock URL。 func (m *MockStorage) GetURL(path string) string { return "http://mock.test" + path } // AssertEqual 断言相等。 func AssertEqual(t *testing.T, expected, actual any) { t.Helper() if expected != actual { t.Errorf("不相等: 期望 %v, 实际 %v", expected, actual) } } // AssertNotNil 断言非空。 func AssertNotNil(t *testing.T, value any) { t.Helper() if value == nil { t.Error("期望非空,实际为空") } } // AssertNil 断言为空。 func AssertNil(t *testing.T, value any) { t.Helper() if value != nil { t.Errorf("期望为空,实际为: %v", value) } } // AssertTrue 断言为 true。 func AssertTrue(t *testing.T, value bool) { t.Helper() if !value { t.Error("期望为 true,实际为 false") } } // AssertFalse 断言为 false。 func AssertFalse(t *testing.T, value bool) { t.Helper() if value { t.Error("期望为 false,实际为 true") } } // AssertError 断言有错误。 func AssertError(t *testing.T, err error) { t.Helper() if err == nil { t.Error("期望有错误,实际无错误") } } // AssertNoError 断言无错误。 func AssertNoError(t *testing.T, err error) { t.Helper() if err != nil { t.Errorf("期望无错误,实际错误: %v", err) } }