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"]) } }