Files
user-system/internal/api/handler/webhook_handler_test.go
long-agent e00af0bce4 fix: unify handler response format in log, permission, webhook handlers
- log_handler.go: Fix GetMyLoginLogs/GetMyOperationLogs/GetLoginLogs/GetOperationLogs to use {code, message, data}
- permission_handler.go: Fix all error responses to use {code, message}
- webhook_handler.go: Add missing "message" field in success responses, wrap data in data object with list/total/page/page_size
- webhook_handler_test.go: Update test to match new response format

Standardize all JSON responses to {code: 0, message: "success", data: ...} for success
and {code: XXX, message: "..."} for errors.
2026-04-11 13:12:27 +08:00

450 lines
13 KiB
Go

package handler_test
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/api/handler"
"github.com/user-management-system/internal/api/middleware"
"github.com/user-management-system/internal/api/router"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/cache"
"github.com/user-management-system/internal/config"
"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"
)
var webhookDbCounter int64
// doRequestWithCheck 执行HTTP请求并在失败时t.Fatalf
func doRequestWithCheck(t *testing.T, method, url string, token string, body interface{}) *http.Response {
t.Helper()
var bodyReader io.Reader
if body != nil {
jsonBytes, _ := json.Marshal(body)
bodyReader = bytes.NewReader(jsonBytes)
}
req, err := http.NewRequest(method, url, bodyReader)
if err != nil {
t.Fatalf("create request failed: %v", err)
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatalf("request failed: %v", err)
}
return resp
}
func setupWebhookTestServer(t *testing.T) (*httptest.Server, *gorm.DB, string, func()) {
t.Helper()
gin.SetMode(gin.TestMode)
id := atomic.AddInt64(&webhookDbCounter, 1)
dsn := fmt.Sprintf("file:webhookdb_%d_%s?mode=memory&cache=shared", id, t.Name())
db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{
DriverName: "sqlite",
DSN: dsn,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Skipf("skipping webhook handler test (SQLite unavailable): %v", err)
return nil, nil, "", func() {}
}
if err := db.AutoMigrate(
&domain.User{},
&domain.Role{},
&domain.Permission{},
&domain.UserRole{},
&domain.RolePermission{},
&domain.Device{},
&domain.LoginLog{},
&domain.OperationLog{},
&domain.PasswordHistory{},
&domain.Webhook{},
&domain.WebhookDelivery{},
); err != nil {
t.Fatalf("db migration failed: %v", err)
}
jwtManager, err := auth.NewJWTWithOptions(auth.JWTOptions{
HS256Secret: "test-webhook-secret-key",
AccessTokenExpire: 15 * time.Minute,
RefreshTokenExpire: 7 * 24 * time.Hour,
})
if err != nil {
t.Fatalf("create jwt manager failed: %v", err)
}
l1Cache := cache.NewL1Cache()
l2Cache := cache.NewRedisCache(false)
cacheManager := cache.NewCacheManager(l1Cache, l2Cache)
userRepo := repository.NewUserRepository(db)
roleRepo := repository.NewRoleRepository(db)
permissionRepo := repository.NewPermissionRepository(db)
userRoleRepo := repository.NewUserRoleRepository(db)
rolePermissionRepo := repository.NewRolePermissionRepository(db)
authSvc := service.NewAuthService(userRepo, nil, jwtManager, cacheManager, 8, 5, 15*time.Minute)
authSvc.SetRoleRepositories(userRoleRepo, roleRepo)
webhookSvc := service.NewWebhookService(db)
rateLimitCfg := config.RateLimitConfig{}
rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg)
authMiddleware := middleware.NewAuthMiddleware(
jwtManager, userRepo, userRoleRepo, roleRepo, rolePermissionRepo, permissionRepo, l1Cache,
)
authMiddleware.SetCacheManager(cacheManager)
authHandler := handler.NewAuthHandler(authSvc)
webhookHandler := handler.NewWebhookHandler(webhookSvc)
r := router.NewRouter(
authHandler, nil, nil, nil, nil, nil,
authMiddleware, rateLimitMiddleware, nil,
nil, nil, nil, webhookHandler,
nil, nil, nil, nil, nil, nil, nil, nil, nil,
)
engine := r.Setup()
server := httptest.NewServer(engine)
// Register a user and get token
registerReq := map[string]interface{}{
"username": fmt.Sprintf("webhookuser_%d", time.Now().UnixNano()),
"password": "TestPass123!",
"email": fmt.Sprintf("webhook_%d@test.com", time.Now().UnixNano()),
}
jsonBytes, _ := json.Marshal(registerReq)
regResp, _ := http.Post(server.URL+"/api/v1/auth/register", "application/json", bytes.NewReader(jsonBytes))
io.ReadAll(regResp.Body)
regResp.Body.Close()
// Login to get token
loginReq := map[string]interface{}{
"username": registerReq["username"],
"password": registerReq["password"],
}
jsonBytes, _ = json.Marshal(loginReq)
loginResp, _ := http.Post(server.URL+"/api/v1/auth/login", "application/json", bytes.NewReader(jsonBytes))
var loginResult struct {
Data struct {
AccessToken string `json:"access_token"`
} `json:"data"`
}
json.NewDecoder(loginResp.Body).Decode(&loginResult)
loginResp.Body.Close()
token := loginResult.Data.AccessToken
return server, db, token, func() {
server.Close()
if sqlDB, err := db.DB(); err == nil {
sqlDB.Close()
}
}
}
func TestWebhookHandler_CreateWebhook_Success(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
reqBody := map[string]interface{}{
"name": "Test Webhook",
"url": "https://example.com/webhook",
"events": []string{"user.created", "user.deleted"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, reqBody)
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected status 201, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["code"].(float64) != 0 {
t.Fatalf("expected code 0, got %v", result["code"])
}
if result["data"] == nil {
t.Fatal("expected data in response")
}
}
func TestWebhookHandler_CreateWebhook_InvalidURL(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
reqBody := map[string]interface{}{
"name": "Test Webhook",
"url": "not-a-valid-url",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, reqBody)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", resp.StatusCode)
}
}
func TestWebhookHandler_CreateWebhook_MissingName(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
reqBody := map[string]interface{}{
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, reqBody)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", resp.StatusCode)
}
}
func TestWebhookHandler_ListWebhooks_Success(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
// Create a webhook first
reqBody := map[string]interface{}{
"name": "List Test Webhook",
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, reqBody)
resp.Body.Close()
// List webhooks
resp = doRequestWithCheck(t, "GET", server.URL+"/api/v1/webhooks?page=1&page_size=10", token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["code"].(float64) != 0 {
t.Fatalf("expected code 0, got %v", result["code"])
}
data := result["data"].(map[string]interface{})
if data["total"] == nil {
t.Fatal("expected total in response")
}
}
func TestWebhookHandler_UpdateWebhook_Success(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
// Create a webhook first
createReq := map[string]interface{}{
"name": "Original Name",
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, createReq)
var createResult map[string]interface{}
json.NewDecoder(resp.Body).Decode(&createResult)
resp.Body.Close()
webhookID := createResult["data"].(map[string]interface{})["id"].(float64)
// Update the webhook
updateReq := map[string]interface{}{
"name": "Updated Name",
}
resp = doRequestWithCheck(t, "PUT", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f", webhookID), token, updateReq)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["code"].(float64) != 0 {
t.Fatalf("expected code 0, got %v", result["code"])
}
}
func TestWebhookHandler_UpdateWebhook_InvalidID(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
updateReq := map[string]interface{}{
"name": "Updated Name",
}
resp := doRequestWithCheck(t, "PUT", server.URL+"/api/v1/webhooks/invalid", token, updateReq)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", resp.StatusCode)
}
}
func TestWebhookHandler_DeleteWebhook_Success(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
// Create a webhook first
createReq := map[string]interface{}{
"name": "Delete Test Webhook",
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, createReq)
var createResult map[string]interface{}
json.NewDecoder(resp.Body).Decode(&createResult)
resp.Body.Close()
webhookID := createResult["data"].(map[string]interface{})["id"].(float64)
// Delete the webhook
resp = doRequestWithCheck(t, "DELETE", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f", webhookID), token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["code"].(float64) != 0 {
t.Fatalf("expected code 0, got %v", result["code"])
}
}
func TestWebhookHandler_DeleteWebhook_NotFound(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
resp := doRequestWithCheck(t, "DELETE", server.URL+"/api/v1/webhooks/99999", token, nil)
defer resp.Body.Close()
// Delete is idempotent - returns 200 even if not found
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}
}
func TestWebhookHandler_GetWebhookDeliveries_Success(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
// Create a webhook first
createReq := map[string]interface{}{
"name": "Deliveries Test Webhook",
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp := doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, createReq)
var createResult map[string]interface{}
json.NewDecoder(resp.Body).Decode(&createResult)
resp.Body.Close()
webhookID := createResult["data"].(map[string]interface{})["id"].(float64)
// Get webhook deliveries
resp = doRequestWithCheck(t, "GET", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f/deliveries?limit=20", webhookID), token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
t.Fatalf("expected status 200, got %d: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
if result["code"].(float64) != 0 {
t.Fatalf("expected code 0, got %v", result["code"])
}
if result["data"] == nil {
t.Fatal("expected data in response")
}
}
func TestWebhookHandler_GetWebhookDeliveries_InvalidID(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
resp := doRequestWithCheck(t, "GET", server.URL+"/api/v1/webhooks/invalid/deliveries", token, nil)
defer resp.Body.Close()
if resp.StatusCode != http.StatusBadRequest {
t.Fatalf("expected status 400, got %d", resp.StatusCode)
}
}
func TestWebhookHandler_ListWebhooks_Pagination(t *testing.T) {
server, _, token, cleanup := setupWebhookTestServer(t)
defer cleanup()
// Create multiple webhooks
var resp *http.Response
for i := 0; i < 3; i++ {
reqBody := map[string]interface{}{
"name": fmt.Sprintf("Pagination Test Webhook %d", i),
"url": "https://example.com/webhook",
"events": []string{"user.created"},
}
resp = doRequestWithCheck(t, "POST", server.URL+"/api/v1/webhooks", token, reqBody)
resp.Body.Close()
}
// Test pagination
resp = doRequestWithCheck(t, "GET", server.URL+"/api/v1/webhooks?page=1&page_size=2", token, nil)
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
data := result["data"].(map[string]interface{})
list := data["list"].([]interface{})
if len(list) != 2 {
t.Fatalf("expected 2 webhooks per page, got %d", len(list))
}
if data["page"].(float64) != 1 {
t.Fatalf("expected page 1, got %v", data["page"])
}
if data["page_size"].(float64) != 2 {
t.Fatalf("expected page_size 2, got %v", data["page_size"])
}
}