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)) }