443 lines
14 KiB
Go
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))
|
|
}
|