From 3ffce94caf31deaa5f0b7fec8ec46e39732e8c0d Mon Sep 17 00:00:00 2001 From: long-agent Date: Thu, 9 Apr 2026 11:48:48 +0800 Subject: [PATCH] test: add WebhookHandler tests Add comprehensive tests for WebhookHandler: - TestWebhookHandler_CreateWebhook_Success - TestWebhookHandler_CreateWebhook_InvalidURL - TestWebhookHandler_CreateWebhook_MissingName - TestWebhookHandler_ListWebhooks_Success - TestWebhookHandler_UpdateWebhook_Success - TestWebhookHandler_UpdateWebhook_InvalidID - TestWebhookHandler_DeleteWebhook_Success - TestWebhookHandler_DeleteWebhook_NotFound - TestWebhookHandler_GetWebhookDeliveries_Success - TestWebhookHandler_GetWebhookDeliveries_InvalidID - TestWebhookHandler_ListWebhooks_Pagination --- internal/api/handler/webhook_handler_test.go | 482 +++++++++++++++++++ 1 file changed, 482 insertions(+) create mode 100644 internal/api/handler/webhook_handler_test.go diff --git a/internal/api/handler/webhook_handler_test.go b/internal/api/handler/webhook_handler_test.go new file mode 100644 index 0000000..77598c1 --- /dev/null +++ b/internal/api/handler/webhook_handler_test.go @@ -0,0 +1,482 @@ +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 + +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"}, + } + jsonBytes, _ := json.Marshal(reqBody) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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"}, + } + jsonBytes, _ := json.Marshal(reqBody) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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"}, + } + jsonBytes, _ := json.Marshal(reqBody) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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"}, + } + jsonBytes, _ := json.Marshal(reqBody) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + http.DefaultClient.Do(req) + + // List webhooks + req, _ = http.NewRequest("GET", server.URL+"/api/v1/webhooks?page=1&page_size=10", nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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") + } + if result["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"}, + } + jsonBytes, _ := json.Marshal(createReq) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + 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", + } + jsonBytes, _ = json.Marshal(updateReq) + req, _ = http.NewRequest("PUT", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f", webhookID), bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ = http.DefaultClient.Do(req) + 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", + } + jsonBytes, _ := json.Marshal(updateReq) + req, _ := http.NewRequest("PUT", server.URL+"/api/v1/webhooks/invalid", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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"}, + } + jsonBytes, _ := json.Marshal(createReq) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + 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 + req, _ = http.NewRequest("DELETE", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f", webhookID), nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ = http.DefaultClient.Do(req) + 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() + + req, _ := http.NewRequest("DELETE", server.URL+"/api/v1/webhooks/99999", nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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"}, + } + jsonBytes, _ := json.Marshal(createReq) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + 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 + req, _ = http.NewRequest("GET", server.URL+fmt.Sprintf("/api/v1/webhooks/%.0f/deliveries?limit=20", webhookID), nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ = http.DefaultClient.Do(req) + 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() + + req, _ := http.NewRequest("GET", server.URL+"/api/v1/webhooks/invalid/deliveries", nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + 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 + 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"}, + } + jsonBytes, _ := json.Marshal(reqBody) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/webhooks", bytes.NewReader(jsonBytes)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+token) + resp, _ := http.DefaultClient.Do(req) + resp.Body.Close() + } + + // Test pagination + req, _ := http.NewRequest("GET", server.URL+"/api/v1/webhooks?page=1&page_size=2", nil) + req.Header.Set("Authorization", "Bearer "+token) + + resp, _ := http.DefaultClient.Do(req) + defer resp.Body.Close() + + var result map[string]interface{} + json.NewDecoder(resp.Body).Decode(&result) + + data := result["data"].([]interface{}) + if len(data) != 2 { + t.Fatalf("expected 2 webhooks per page, got %d", len(data)) + } + + if result["page"].(float64) != 1 { + t.Fatalf("expected page 1, got %v", result["page"]) + } + if result["page_size"].(float64) != 2 { + t.Fatalf("expected page_size 2, got %v", result["page_size"]) + } +}