dcfd24b624
v1.0.3 定位为 bug fix release,收口 v1.0.2 引入的破坏性清理并修复 4 个轻量 bug + 依赖复查 + 版本号治理 + 文档对齐。同时包含此前未提交 的 v1.0.2 全部工作。 v1.0.3 变更: Fixed: - generateJTI 忽略 rand.Read 错误(jwt)— 改为 (string, error) 并传播 - QueryBuilder.Page Count 受残留 Limit 截断(repository)— countDB 加 Limit(-1).Offset(-1) - OSS/本地存储文件名冲突(storage)— 新增 uniqueFilename 加 8 字节随机后缀 - 数据库重试策略对不可恢复错误无效(database)— 新增 isTransientDBError Dependencies: - go mod tidy 补全 postgres 方言传递依赖;安全补丁升级 x/crypto v0.49→v0.53、golang-jwt/jwt/v5 v5.2.1→v5.3.1、gorilla/websocket v1.5.1→v1.5.3 Removed: - Breaking: 清理 v1.0.2 兼容别名(InitMySQL* / driverName) - 死代码 database.DBResolver - 代码中 292 行"评分/理由"自夸注释(#26,23 个文件) Changed: - Breaking: 错误码体系重构 CodeSuccess 1→0、CodeFail 0→1,删除 CodeInvalidParams,加编译期防撞码 - database/mysql.go → manager.go;Logger 拆分三独立 core 修复 Tee 重复写入 - 版本号常量化:app.go 新增 const Version 作为唯一来源,CLI/脚手架模板引用之,不再散落字面量 Security: - CORS 中间件按 W3C 规范修复 Allow-Credentials / Vary: Origin / 非白名单不回显 Added: - console 包显式 level 控制(SetLevel/WithLevel/LevelSilent,atomic.Int32 并发安全) - 新增测试 jwt/repository/storage/database/router/middleware/app - 文档:新增 CHANGELOG.md、Version_v1.0.2_report.md;更新 README/GUIDE 顶部更新日志 - 文档对齐:删除对不存在的 config.DefaultManager() getter 的描述(保持 API 与文档一致) 详见 CHANGELOG.md 与 README.md 更新日志。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
323 lines
7.1 KiB
Go
323 lines
7.1 KiB
Go
package response_test
|
|
|
|
import (
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/EthanCodeCraft/xlgo-core/response"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
func setupTestRouter() *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
return r
|
|
}
|
|
|
|
func TestSuccess(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.Success(c, gin.H{"message": "ok"})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Success status = %d", w.Code)
|
|
}
|
|
|
|
// 验证响应体包含 code=0
|
|
body := w.Body.String()
|
|
if !contains(body, `"code":0`) {
|
|
t.Errorf("Success body should contain code:0, got %s", body)
|
|
}
|
|
}
|
|
|
|
func TestSuccessWithMsg(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.SuccessWithMsg(c, "操作成功", gin.H{"id": 1})
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("SuccessWithMsg status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestFail(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.Fail(c, "参数错误")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Fail status = %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !contains(body, `"code":1`) {
|
|
t.Errorf("Fail body should contain code:1, got %s", body)
|
|
}
|
|
}
|
|
|
|
func TestFailWithCode(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.FailWithCode(c, response.CodeUnauthorized, "未授权")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("FailWithCode status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestUnauthorized(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.Unauthorized(c, "请先登录")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Unauthorized status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestNotFound(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.NotFound(c, "资源不存在")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("NotFound status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestServerError(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.ServerError(c, "服务器错误")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("ServerError status = %d", w.Code)
|
|
}
|
|
}
|
|
|
|
func TestRateLimit(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.RateLimit(c)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("RateLimit status = %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !contains(body, `"code":`) {
|
|
t.Errorf("RateLimit body should contain code, got %s", body)
|
|
}
|
|
}
|
|
|
|
func TestPage(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
items := []string{"a", "b", "c"}
|
|
response.Page(c, items, 100, 1, 20)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Page status = %d", w.Code)
|
|
}
|
|
|
|
body := w.Body.String()
|
|
if !contains(body, `"total":100`) {
|
|
t.Errorf("Page body should contain total:100, got %s", body)
|
|
}
|
|
}
|
|
|
|
func TestDownload(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
data := []byte("test file content")
|
|
response.Download(c, "test.txt", data)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("Download status = %d", w.Code)
|
|
}
|
|
|
|
// 验证响应头
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/octet-stream" {
|
|
t.Errorf("Download Content-Type = %s, want application/octet-stream", contentType)
|
|
}
|
|
|
|
disposition := w.Header().Get("Content-Disposition")
|
|
if !contains(disposition, "test.txt") {
|
|
t.Errorf("Download Content-Disposition should contain filename, got %s", disposition)
|
|
}
|
|
}
|
|
|
|
func TestDownloadWithContentType(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
data := []byte("test file content")
|
|
response.DownloadWithContentType(c, "test.pdf", "application/pdf", data)
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("DownloadWithContentType status = %d", w.Code)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if contentType != "application/pdf" {
|
|
t.Errorf("DownloadWithContentType = %s, want application/pdf", contentType)
|
|
}
|
|
}
|
|
|
|
func TestHTML(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.HTML(c, "<html><body>Hello</body></html>")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != 200 {
|
|
t.Errorf("HTML status = %d", w.Code)
|
|
}
|
|
|
|
contentType := w.Header().Get("Content-Type")
|
|
if !contains(contentType, "text/html") {
|
|
t.Errorf("HTML Content-Type should be text/html, got %s", contentType)
|
|
}
|
|
}
|
|
|
|
func TestRedirect(t *testing.T) {
|
|
r := setupTestRouter()
|
|
r.GET("/test", func(c *gin.Context) {
|
|
response.Redirect(c, 302, "/new-location")
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// 重定向返回 302
|
|
if w.Code != 302 {
|
|
t.Errorf("Redirect status = %d, want 302", w.Code)
|
|
}
|
|
|
|
location := w.Header().Get("Location")
|
|
if location != "/new-location" {
|
|
t.Errorf("Redirect Location = %s, want /new-location", location)
|
|
}
|
|
}
|
|
|
|
func TestErrorCodes(t *testing.T) {
|
|
// 测试错误码定义
|
|
if response.CodeSuccess != 0 {
|
|
t.Errorf("CodeSuccess = %d, want 0", response.CodeSuccess)
|
|
}
|
|
if response.CodeFail != 1 {
|
|
t.Errorf("CodeFail = %d, want 1", response.CodeFail)
|
|
}
|
|
if response.CodeUnauthorized == 0 {
|
|
t.Error("CodeUnauthorized should not be 0")
|
|
}
|
|
if response.CodeNotFound == 0 {
|
|
t.Error("CodeNotFound should not be 0")
|
|
}
|
|
if response.CodeServerError == 0 {
|
|
t.Error("CodeServerError should not be 0")
|
|
}
|
|
if response.CodeRateLimit == 0 {
|
|
t.Error("CodeRateLimit should not be 0")
|
|
}
|
|
}
|
|
|
|
func TestResponseStructure(t *testing.T) {
|
|
// 测试 Response 结构体
|
|
resp := response.Response{
|
|
Code: 0,
|
|
Msg: "成功",
|
|
Data: gin.H{"id": 1},
|
|
RequestID: "test-123",
|
|
}
|
|
|
|
if resp.Code != 0 {
|
|
t.Error("Response Code failed")
|
|
}
|
|
if resp.Msg != "成功" {
|
|
t.Error("Response Msg failed")
|
|
}
|
|
}
|
|
|
|
func TestPageDataStructure(t *testing.T) {
|
|
// 测试 PageData 结构体
|
|
data := response.PageData{
|
|
Items: []string{"a", "b"},
|
|
Total: 100,
|
|
Page: 1,
|
|
PageSize: 20,
|
|
}
|
|
|
|
if data.Total != 100 {
|
|
t.Error("PageData Total failed")
|
|
}
|
|
}
|
|
|
|
func contains(s, sub string) bool {
|
|
for i := 0; i <= len(s)-len(sub); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
} |