Files
gochat/internal/handler/api/v1/integration_hook_handler_suite_test.go
T
2026-06-04 15:44:48 +08:00

465 lines
15 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
)
// IntegrationHookHandlerSuite tests IntegrationHookHandler with real SQLite DB + real service.
type IntegrationHookHandlerSuite struct {
suite.Suite
db *gorm.DB
router *gin.Engine
handler *IntegrationHookHandler
}
func (s *IntegrationHookHandlerSuite) SetupSuite() {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
s.Require().NoError(err, "failed to open SQLite test database")
s.Require().NoError(db.AutoMigrate(
&model.Account{},
&model.User{},
&model.AccountUser{},
&model.Inbox{},
&model.IntegrationHook{},
&model.IntegrationApp{},
), "failed to auto-migrate models")
s.db = db
hookRepo := repository.NewIntegrationHookRepo(db)
appRepo := repository.NewIntegrationAppRepo(db)
svc := service.NewIntegrationHookService(hookRepo, appRepo, nil) // nil registry for handler tests
s.handler = NewIntegrationHookHandler(svc)
r := gin.New()
r.Use(gin.Recovery(), mockAuthMiddlewareForHook())
g := r.Group("/api/v1/accounts/:account_id/integrations")
apps := g.Group("/apps")
{
apps.GET("/", s.handler.ListApps)
apps.GET("/:id", s.handler.GetApp)
}
hooks := g.Group("/hooks")
{
hooks.GET("/", s.handler.ListHooks)
hooks.GET("/:id", s.handler.GetHook)
hooks.POST("/", s.handler.CreateHook)
hooks.PUT("/:id", s.handler.UpdateHook)
hooks.DELETE("/:id", s.handler.DeleteHook)
hooks.POST("/:id/process_event", s.handler.ProcessHookEvent)
}
s.router = r
}
func (s *IntegrationHookHandlerSuite) TearDownSuite() {
sqlDB, err := s.db.DB()
if err == nil {
sqlDB.Close()
}
}
func (s *IntegrationHookHandlerSuite) SetupTest() {
s.db.Exec("DELETE FROM integration_hooks")
s.db.Exec("DELETE FROM integration_apps")
}
func (s *IntegrationHookHandlerSuite) uid(id uint) string {
return strconv.FormatUint(uint64(id), 10)
}
// --- Helper: create an IntegrationApp directly in DB ---
func (s *IntegrationHookHandlerSuite) createApp(name string, hookType model.HookType) *model.IntegrationApp {
app := &model.IntegrationApp{
Name: name,
HookType: hookType,
Enabled: true,
}
s.Require().NoError(s.db.Create(app).Error)
return app
}
// --- Helper: create an IntegrationHook directly in DB ---
var hookTokenCounter = 0
func (s *IntegrationHookHandlerSuite) createHook(accountID uint, hookType model.HookType, url string) *model.IntegrationHook {
hookTokenCounter++
token := "test_token_" + strconv.Itoa(hookTokenCounter)
hook := &model.IntegrationHook{
AccountID: accountID,
HookType: hookType,
Status: model.HookStatusActive,
URL: url,
AccessToken: token,
}
s.Require().NoError(s.db.Create(hook).Error)
return hook
}
// =====================
// ListApps tests
// =====================
func (s *IntegrationHookHandlerSuite) TestListApps_Success() {
s.createApp("Webhook App", model.HookTypeWebhook)
s.createApp("Slack App", model.HookTypeSlack)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].([]interface{})
s.Len(data, 2)
}
func (s *IntegrationHookHandlerSuite) TestListApps_Empty() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].([]interface{})
s.Len(data, 0)
}
// =====================
// GetApp tests
// =====================
func (s *IntegrationHookHandlerSuite) TestGetApp_Success() {
app := s.createApp("Slack App", model.HookTypeSlack)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/"+s.uid(app.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].(map[string]interface{})
s.Equal("Slack App", data["name"])
}
func (s *IntegrationHookHandlerSuite) TestGetApp_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestGetApp_NotFound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/9999", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// =====================
// ListHooks tests
// =====================
func (s *IntegrationHookHandlerSuite) TestListHooks_Success() {
s.createHook(1, model.HookTypeWebhook, "https://example.com/hook1")
s.createHook(1, model.HookTypeSlack, "https://example.com/hook2")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/hooks/", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].([]interface{})
s.Len(data, 2)
}
func (s *IntegrationHookHandlerSuite) TestListHooks_Empty() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/hooks/", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].([]interface{})
s.Len(data, 0)
}
// =====================
// GetHook tests
// =====================
func (s *IntegrationHookHandlerSuite) TestGetHook_Success() {
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var body map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &body))
s.Equal(true, body["success"])
data := body["data"].(map[string]interface{})
s.Equal("webhook", data["hook_type"])
s.Equal("https://example.com/hook", data["url"])
}
func (s *IntegrationHookHandlerSuite) TestGetHook_NotFound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/hooks/9999", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// =====================
// CreateHook tests
// =====================
func (s *IntegrationHookHandlerSuite) TestCreateHook_Success() {
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{
"hook_type": "webhook",
"url": "https://example.com/new_hook",
}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(true, resp["success"])
data := resp["data"].(map[string]interface{})
s.Equal("webhook", data["hook_type"])
s.Equal("https://example.com/new_hook", data["url"])
s.NotNil(data["id"])
}
func (s *IntegrationHookHandlerSuite) TestCreateHook_InvalidJSON() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/", bytes.NewBufferString("{invalid"))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestCreateHook_InvalidAccountID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/integrations/hooks/", bytes.NewBufferString(`{"hook_type":"webhook","url":"https://example.com"}`))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
// =====================
// UpdateHook tests
// =====================
func (s *IntegrationHookHandlerSuite) TestUpdateHook_Success() {
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{
"url": "https://example.com/updated_hook",
"status": "inactive",
}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID), bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(true, resp["success"])
data := resp["data"].(map[string]interface{})
s.Equal("https://example.com/updated_hook", data["url"])
s.Equal("inactive", data["status"])
}
func (s *IntegrationHookHandlerSuite) TestUpdateHook_NotFound() {
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{
"url": "https://example.com/updated",
}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/integrations/hooks/9999", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestUpdateHook_InvalidJSON() {
s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/integrations/hooks/1", bytes.NewBufferString("{bad"))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestUpdateHook_InvalidID() {
body, _ := json.Marshal(map[string]interface{}{"hook": map[string]string{"url": "https://example.com/updated"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/integrations/hooks/abc", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
// =====================
// DeleteHook tests
// =====================
func (s *IntegrationHookHandlerSuite) TestDeleteHook_Success() {
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(true, resp["success"])
data := resp["data"].(map[string]interface{})
s.Equal(float64(hook.ID), data["id"])
}
func (s *IntegrationHookHandlerSuite) TestDeleteHook_NotFound() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/hooks/9999", nil)
s.router.ServeHTTP(w, req)
// GORM soft-delete on non-existent ID returns nil error → handler returns 200 with {"id": 9999}
s.Equal(http.StatusOK, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestDeleteHook_InvalidID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/integrations/hooks/abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
// =====================
// ProcessHookEvent tests
// =====================
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_Success() {
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
eventData, _ := json.Marshal(map[string]interface{}{
"event": "message_created",
"content": "Hello world",
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID)+"/process_event", bytes.NewBuffer(eventData))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(true, resp["success"])
data := resp["data"].(map[string]interface{})
s.Equal("event processed", data["message"])
}
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_NotFound() {
eventData, _ := json.Marshal(map[string]interface{}{"event": "test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/9999/process_event", bytes.NewBuffer(eventData))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_InvalidJSON() {
hook := s.createHook(1, model.HookTypeWebhook, "https://example.com/hook")
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID)+"/process_event", bytes.NewBufferString("{bad"))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_InvalidHookID() {
eventData, _ := json.Marshal(map[string]interface{}{"event": "test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/abc/process_event", bytes.NewBuffer(eventData))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *IntegrationHookHandlerSuite) TestProcessHookEvent_InactiveHook() {
// Create a hook with inactive status
hookTokenCounter++
hook := &model.IntegrationHook{
AccountID: 1,
HookType: model.HookTypeWebhook,
Status: model.HookStatusInactive,
URL: "https://example.com/hook",
AccessToken: "test_token_inactive",
}
s.Require().NoError(s.db.Create(hook).Error)
eventData, _ := json.Marshal(map[string]interface{}{"event": "test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/integrations/hooks/"+s.uid(hook.ID)+"/process_event", bytes.NewBuffer(eventData))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
// Inactive hook returns service error → 500 via handleServiceError
s.True(w.Code == http.StatusUnprocessableEntity || w.Code == http.StatusBadRequest)
}
// TestIntegrationHookHandlerSuite runs the suite.
func TestIntegrationHookHandlerSuite(t *testing.T) {
suite.Run(t, new(IntegrationHookHandlerSuite))
}