test: 增强 handler/middleware 测试覆盖并优化错误分类
测试增强: - handler_test.go: 大幅增强 handler 集成测试(+1284/-98 行) - theme_handler_test.go: 增强主题管理测试(+174/-22 行) - auth_bootstrap_test.go: 新增 bootstrap 认证测试(+329 行) - ratelimit_test.go: 新增限流中间件测试(+153 行) - runtime_test.go: 新增运行时中间件测试(+351 行) 错误处理: - auth_handler.go: classifyErrorMessage 增加 TOTP 错误码和 2FA 状态字分类 清理: - 删除覆盖率报告残留文件(coverage_issue, handler, middleware 等) - 归档 docs/superpowers/plans/2026-05-09-middleware-test-backfill-phase1.md
This commit is contained in:
@@ -784,13 +784,17 @@ func classifyErrorMessage(msg string) int {
|
||||
return http.StatusNotFound
|
||||
case contains(lower, "already exists", "已存在", "已注册", "duplicate"):
|
||||
return http.StatusConflict
|
||||
case contains(lower, "验证码错误", "验证码或恢复码错误", "verification code", "recovery code"):
|
||||
return http.StatusUnauthorized
|
||||
case contains(lower, "unauthorized", "invalid token", "token", "令牌", "未认证"):
|
||||
return http.StatusUnauthorized
|
||||
case contains(lower, "forbidden", "permission", "权限", "禁止"):
|
||||
return http.StatusForbidden
|
||||
case contains(lower, "2fa 已", "2fa 未", "请先初始化 2fa", "已启用", "未启用"):
|
||||
return http.StatusBadRequest
|
||||
case contains(lower, "invalid", "required", "must", "cannot be empty", "不能为空",
|
||||
"格式", "参数", "密码不正确", "incorrect", "wrong", "too short", "too long",
|
||||
"已失效", "expired", "验证码不正确", "不能与"):
|
||||
"已失效", "expired", "验证码不正确", "不能与", "不能删除自己", "不能删除最后一个管理员"):
|
||||
return http.StatusBadRequest
|
||||
case contains(lower, "locked", "too many", "账号已被锁定", "rate limit"):
|
||||
return http.StatusTooManyRequests
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,10 +17,6 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Theme Handler Tests - TDD approach
|
||||
// =============================================================================
|
||||
|
||||
func setupThemeTestEnv(t *testing.T) (*handler.ThemeHandler, *gorm.DB) {
|
||||
t.Helper()
|
||||
gin.SetMode(gin.TestMode)
|
||||
@@ -45,10 +41,22 @@ func setupThemeTestEnv(t *testing.T) (*handler.ThemeHandler, *gorm.DB) {
|
||||
return handler.NewThemeHandler(themeSvc), db
|
||||
}
|
||||
|
||||
func createThemeForTest(t *testing.T, h *handler.ThemeHandler, body string) {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("POST", "/api/v1/themes", bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
h.CreateTheme(c)
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Fatalf("create theme failed: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemeHandler_CreateTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("创建主题成功", func(t *testing.T) {
|
||||
t.Run("create success", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"name":"test-theme","primary_color":"#1976d2"}`
|
||||
@@ -58,20 +66,19 @@ func TestThemeHandler_CreateTheme(t *testing.T) {
|
||||
h.CreateTheme(c)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusCreated, w.Code)
|
||||
t.Fatalf("expected status %d, got %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("解析响应失败: %v", err)
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
|
||||
if resp["code"].(float64) != 0 {
|
||||
t.Errorf("期望 code=0, 得到 %v", resp["code"])
|
||||
t.Fatalf("expected code=0, got %v", resp["code"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("创建主题失败-缺少名称", func(t *testing.T) {
|
||||
t.Run("create missing name", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
body := `{"primary_color":"#1976d2"}`
|
||||
@@ -81,31 +88,30 @@ func TestThemeHandler_CreateTheme(t *testing.T) {
|
||||
h.CreateTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_ListThemes(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
createThemeForTest(t, h, `{"name":"list-theme","primary_color":"#1976d2"}`)
|
||||
|
||||
t.Run("获取主题列表", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes", nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes", nil)
|
||||
|
||||
h.ListThemes(c)
|
||||
h.ListThemes(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemeHandler_GetTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("获取主题失败-无效ID", func(t *testing.T) {
|
||||
t.Run("get invalid id", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "invalid"}}
|
||||
@@ -114,7 +120,70 @@ func TestThemeHandler_GetTheme(t *testing.T) {
|
||||
h.GetTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get success", func(t *testing.T) {
|
||||
createThemeForTest(t, h, `{"name":"get-theme","primary_color":"#1976d2"}`)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes/1", nil)
|
||||
|
||||
h.GetTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d, body=%s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_UpdateTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
createThemeForTest(t, h, `{"name":"theme-update","primary_color":"#111111"}`)
|
||||
|
||||
t.Run("update success", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
body := `{"primary_color":"#222222","enabled":true}`
|
||||
c.Request = httptest.NewRequest("PUT", "/api/v1/themes/1", bytes.NewReader([]byte(body)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.UpdateTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d, body=%s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update invalid id", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "invalid"}}
|
||||
c.Request = httptest.NewRequest("PUT", "/api/v1/themes/invalid", bytes.NewReader([]byte(`{}`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.UpdateTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("update invalid json", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest("PUT", "/api/v1/themes/1", bytes.NewReader([]byte(`{"primary_color":`)))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
|
||||
h.UpdateTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -122,7 +191,7 @@ func TestThemeHandler_GetTheme(t *testing.T) {
|
||||
func TestThemeHandler_DeleteTheme(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
|
||||
t.Run("删除主题失败-无效ID", func(t *testing.T) {
|
||||
t.Run("delete invalid id", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "invalid"}}
|
||||
@@ -131,7 +200,90 @@ func TestThemeHandler_DeleteTheme(t *testing.T) {
|
||||
h.DeleteTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("期望状态码 %d, 得到 %d", http.StatusBadRequest, w.Code)
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("delete success", func(t *testing.T) {
|
||||
createThemeForTest(t, h, `{"name":"theme-delete","primary_color":"#1976d2"}`)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "1"}}
|
||||
c.Request = httptest.NewRequest("DELETE", "/api/v1/themes/1", nil)
|
||||
|
||||
h.DeleteTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d, body=%s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestThemeHandler_DefaultAndActiveFlows(t *testing.T) {
|
||||
h, _ := setupThemeTestEnv(t)
|
||||
createThemeForTest(t, h, `{"name":"default-theme","primary_color":"#111111","is_default":true}`)
|
||||
createThemeForTest(t, h, `{"name":"other-theme","primary_color":"#222222"}`)
|
||||
|
||||
t.Run("list all themes", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes/all", nil)
|
||||
|
||||
h.ListAllThemes(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get default theme", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes/default", nil)
|
||||
|
||||
h.GetDefaultTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set default invalid id", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "bad"}}
|
||||
c.Request = httptest.NewRequest("PUT", "/api/v1/themes/bad/default", nil)
|
||||
|
||||
h.SetDefaultTheme(c)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("set default success", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Params = gin.Params{{Key: "id", Value: "2"}}
|
||||
c.Request = httptest.NewRequest("PUT", "/api/v1/themes/2/default", nil)
|
||||
|
||||
h.SetDefaultTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d, body=%s", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get active theme", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("GET", "/api/v1/themes/active", nil)
|
||||
|
||||
h.GetActiveTheme(c)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user