Files
gochat/internal/handler/api/v1/integration_hook_handler_suite_test.go
T

443 lines
14 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")
RegisterIntegrationHookRoutes(g, s.handler)
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,
AppID: string(hookType),
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))
data := body["payload"].([]interface{})
s.Len(data, 2)
app := data[0].(map[string]interface{})
s.Contains(app, "hooks")
}
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))
data := body["payload"].([]interface{})
s.Len(data, 0)
}
// =====================
// GetApp tests
// =====================
func (s *IntegrationHookHandlerSuite) TestGetApp_Success() {
s.createApp("Slack App", model.HookTypeSlack)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/slack", 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("slack", body["id"])
s.Equal("Slack App", body["name"])
}
func (s *IntegrationHookHandlerSuite) TestGetApp_UnknownID() {
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/integrations/apps/unknown", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, 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("webhook", body["app_id"])
s.Equal("account", body["hook_type"])
s.Equal(true, body["status"])
}
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{}{
"app_id": "webhook",
"settings": map[string]interface{}{
"project_id": "project-1",
},
})
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("webhook", resp["app_id"])
s.Equal("account", resp["hook_type"])
s.Equal(true, resp["status"])
s.NotNil(resp["id"])
settings := resp["settings"].(map[string]interface{})
s.Equal("project-1", settings["project_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]interface{}{
"status": "disabled",
"reference_id": "ref-123",
"settings": map[string]interface{}{
"channel": "support",
},
}})
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(false, resp["status"])
s.Equal("ref-123", resp["reference_id"])
settings := resp["settings"].(map[string]interface{})
s.Equal("support", settings["channel"])
}
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)
s.Empty(w.Body.String())
}
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)
s.Equal(http.StatusNotFound, 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("event processed", resp["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,
AppID: "webhook",
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))
}