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
This commit is contained in:
482
internal/api/handler/webhook_handler_test.go
Normal file
482
internal/api/handler/webhook_handler_test.go
Normal file
@@ -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"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user