package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "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/automation" ) // automationDBProvider wraps *gorm.DB to implement automation.DBProvider. type automationDBProvider struct { db *gorm.DB } func (p *automationDBProvider) DB() *gorm.DB { return p.db } // AutomationRuleHandlerTestSuite tests AutomationRuleHandler with real SQLite DB. type AutomationRuleHandlerTestSuite struct { suite.Suite db *gorm.DB router *gin.Engine handler *AutomationRuleHandler } func (s *AutomationRuleHandlerTestSuite) 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(&automation.AutomationRule{}), "failed to auto-migrate AutomationRule model") s.db = db provider := &automationDBProvider{db: db} svc := automation.NewAutomationRuleService(provider) s.handler = NewAutomationRuleHandler(svc) r := gin.New() s.router = r accountGroup := r.Group("/api/v1/accounts/:account_id") { rulesGroup := accountGroup.Group("/automation_rules") { rulesGroup.GET("", s.handler.List) rulesGroup.GET("/:automation_id", s.handler.Get) rulesGroup.POST("", s.handler.Create) rulesGroup.PUT("/:automation_id", s.handler.Update) rulesGroup.DELETE("/:automation_id", s.handler.Delete) rulesGroup.POST("/:automation_id/clone", s.handler.Clone) rulesGroup.POST("/:automation_id/toggle_active", s.handler.ToggleActive) } } } func (s *AutomationRuleHandlerTestSuite) TearDownSuite() { sqlDB, err := s.db.DB() if err == nil { sqlDB.Close() } } func (s *AutomationRuleHandlerTestSuite) SetupTest() { s.db.Exec("DELETE FROM automation_rules") } func (s *AutomationRuleHandlerTestSuite) createRule(accountID uint, eventName, name string, active bool) *automation.AutomationRule { rule := &automation.AutomationRule{ AccountID: accountID, EventName: eventName, Name: name, Active: active, Conditions: automation.Conditions{ {Attribute: "status", FilterOperator: "equal", Values: []string{"open"}, QueryOperator: "and"}, }, Actions: automation.Actions{ {ActionName: "assign_team", ActionParams: map[string]interface{}{"team_id": float64(1)}}, }, } err := s.db.Create(rule).Error s.Require().NoError(err) return rule } // --- List tests --- func (s *AutomationRuleHandlerTestSuite) TestList_Success() { s.createRule(1, "conversation_created", "Rule1", true) s.createRule(1, "message_created", "Rule2", false) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules", 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)) rules := resp["automation_rules"].([]interface{}) s.Equal(2, len(rules)) } func (s *AutomationRuleHandlerTestSuite) TestList_Empty() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules", 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)) rules := resp["automation_rules"].([]interface{}) s.Equal(0, len(rules)) } func (s *AutomationRuleHandlerTestSuite) TestList_InvalidAccountID() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/automation_rules", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } // --- Get tests --- func (s *AutomationRuleHandlerTestSuite) TestGet_Success() { rule := s.createRule(1, "conversation_created", "TestRule", true) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.ID), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestGet_InvalidID() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules/abc", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestGet_NotFound() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules/9999", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } // --- Create tests --- func (s *AutomationRuleHandlerTestSuite) TestCreate_Success() { body := `{"event_name":"conversation_created","name":"New Rule","active":true,"conditions":[{"attribute":"status","filter_operator":"equal","values":["open"],"query_operator":"and"}],"actions":[{"action_name":"assign_team","action_params":{"team_id":1}}]}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusCreated, w.Code) var resp map[string]interface{} s.NoError(json.Unmarshal(w.Body.Bytes(), &resp)) data := resp["data"].(map[string]interface{}) s.Equal("New Rule", data["name"]) s.Equal(float64(1), data["account_id"]) } func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidJSON() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(`{invalid`)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidAccountID() { body := `{"event_name":"conversation_created","name":"Rule","active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/abc/automation_rules", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } // --- Update tests --- func (s *AutomationRuleHandlerTestSuite) TestUpdate_Success() { rule := s.createRule(1, "conversation_created", "OldName", true) body := fmt.Sprintf(`{"event_name":"message_created","name":"UpdatedName","active":false,"conditions":[{"attribute":"status","filter_operator":"equal","values":["resolved"],"query_operator":"and"}],"actions":[{"action_name":"send_message","action_params":{"message":"Hello"}}]}`) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.ID), bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidID() { body := `{"name":"Updated","active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/abc", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidJSON() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/1", bytes.NewBufferString(`{invalid`)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestUpdate_NotFound() { body := `{"name":"Updated","active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/9999", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Service wraps "record not found" error → handleServiceError returns 500 s.Equal(http.StatusUnprocessableEntity, w.Code) } // --- Delete tests --- func (s *AutomationRuleHandlerTestSuite) TestDelete_Success() { rule := s.createRule(1, "conversation_created", "ToDelete", true) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.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["deleted"]) } func (s *AutomationRuleHandlerTestSuite) TestDelete_InvalidID() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, "/api/v1/accounts/1/automation_rules/abc", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestDelete_NotFound() { // GORM Delete on non-existent ID returns nil error — handler returns 200 w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodDelete, "/api/v1/accounts/1/automation_rules/9999", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) } // --- Clone tests --- func (s *AutomationRuleHandlerTestSuite) TestClone_Success() { rule := s.createRule(1, "conversation_created", "OriginalRule", true) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/clone", rule.ID), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusCreated, w.Code) var resp map[string]interface{} s.NoError(json.Unmarshal(w.Body.Bytes(), &resp)) data := resp["data"].(map[string]interface{}) s.Equal("OriginalRule (copy)", data["name"]) s.Equal(false, data["active"]) } func (s *AutomationRuleHandlerTestSuite) TestClone_InvalidID() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/abc/clone", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestClone_NotFound() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/9999/clone", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } // --- ToggleActive tests --- func (s *AutomationRuleHandlerTestSuite) TestToggleActive_Success() { rule := s.createRule(1, "conversation_created", "ToggleRule", false) body := `{"active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/toggle_active", rule.ID), bytes.NewBufferString(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["active"]) } func (s *AutomationRuleHandlerTestSuite) TestToggleActive_SetInactive() { rule := s.createRule(1, "conversation_created", "ActiveRule", true) body := `{"active":false}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/toggle_active", rule.ID), bytes.NewBufferString(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["active"]) } func (s *AutomationRuleHandlerTestSuite) TestToggleActive_InvalidID() { body := `{"active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/abc/toggle_active", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestToggleActive_InvalidJSON() { w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/1/toggle_active", bytes.NewBufferString(`{invalid`)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *AutomationRuleHandlerTestSuite) TestToggleActive_NotFound() { body := `{"active":true}` w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/9999/toggle_active", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // ToggleActive updates via GORM Updates — no error for non-existent, just 0 rows affected s.Equal(http.StatusOK, w.Code) } func TestAutomationRuleHandlerTestSuite(t *testing.T) { suite.Run(t, new(AutomationRuleHandlerTestSuite)) }