- Replace raw http.DefaultClient.Do(req) with doRequestWithCheck helper - Helper function now handles errors via t.Fatalf - Content-Type only set when body is non-nil docs: update REAL_PROJECT_STATUS.md with 2026-04-09 verification Go vet: 0 warnings
451 lines
13 KiB
Go
451 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"])
|
|
}
|
|
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"},
|
|
}
|
|
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"].([]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"])
|
|
}
|
|
}
|