Files
user-system/internal/api/handler/theme_handler_test.go
long-agent b77412b47f 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
2026-05-10 13:46:29 +08:00

290 lines
8.4 KiB
Go

package handler_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/api/handler"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/repository"
"github.com/user-management-system/internal/service"
gormsqlite "gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func setupThemeTestEnv(t *testing.T) (*handler.ThemeHandler, *gorm.DB) {
t.Helper()
gin.SetMode(gin.TestMode)
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: "file:theme_test?mode=memory&cache=shared",
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("failed to connect database: %v", err)
}
if err := db.AutoMigrate(&domain.ThemeConfig{}); err != nil {
t.Fatalf("failed to migrate: %v", err)
}
themeRepo := repository.NewThemeConfigRepository(db)
themeSvc := service.NewThemeService(themeRepo)
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("create success", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"name":"test-theme","primary_color":"#1976d2"}`
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("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("decode response failed: %v", err)
}
if resp["code"].(float64) != 0 {
t.Fatalf("expected code=0, got %v", resp["code"])
}
})
t.Run("create missing name", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body := `{"primary_color":"#1976d2"}`
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.StatusBadRequest {
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"}`)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
c.Request = httptest.NewRequest("GET", "/api/v1/themes", nil)
h.ListThemes(c)
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("get 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("GET", "/api/v1/themes/invalid", nil)
h.GetTheme(c)
if w.Code != http.StatusBadRequest {
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)
}
})
}
func TestThemeHandler_DeleteTheme(t *testing.T) {
h, _ := setupThemeTestEnv(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"}}
c.Request = httptest.NewRequest("DELETE", "/api/v1/themes/invalid", nil)
h.DeleteTheme(c)
if w.Code != http.StatusBadRequest {
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)
}
})
}