package v1 import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/automation" "github.com/gochat/gochat/internal/model" ) // --- BotTriggerConfig Handler Test Suite --- // Uses real SQLite DB + real automation.BotTriggerConfigService. // Tests all handler methods: List, ListByBot, Get, Create, Update, Delete, ToggleActive. // testDBProvider implements automation.DBProvider for tests. type botTriggerConfigTestDBProvider struct { db *gorm.DB } func (p *botTriggerConfigTestDBProvider) DB() *gorm.DB { return p.db } type BotTriggerConfigHandlerTestSuite struct { suite.Suite router *gin.Engine handler *BotTriggerConfigHandler db *gorm.DB svc *automation.BotTriggerConfigService accountID uint agentBotID uint } func TestBotTriggerConfigHandlerSuite(t *testing.T) { suite.Run(t, new(BotTriggerConfigHandlerTestSuite)) } func (s *BotTriggerConfigHandlerTestSuite) 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) // Migrate all models needed (including FK dependencies) s.Require().NoError(db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.AgentBot{}, &model.AgentBotInbox{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, &model.Team{}, &model.TeamMember{}, &automation.BotRule{}, &automation.BotTriggerConfig{}, )) s.db = db // Create a test account account := &model.Account{Name: "Test Account TriggerConfig"} s.Require().NoError(db.Create(account).Error) s.accountID = account.ID // Create a test user user := &model.User{Name: "Test User", Email: "trigger-test@example.com"} s.Require().NoError(db.Create(user).Error) // Create account_user link au := &model.AccountUser{AccountID: s.accountID, UserID: user.ID, Role: "administrator"} s.Require().NoError(db.Create(au).Error) // Create a test AgentBot (account-scoped) accountIDRef := s.accountID bot := &model.AgentBot{ AccountID: &accountIDRef, Name: "Test AgentBot", Description: "Bot for trigger config tests", BotType: "webhook", Secret: "test-secret-trigger", AccessToken: "test-access-token-trigger", } s.Require().NoError(db.Create(bot).Error) s.agentBotID = bot.ID // Create service and handler dbProvider := &botTriggerConfigTestDBProvider{db: db} s.svc = automation.NewBotTriggerConfigService(dbProvider) s.handler = NewBotTriggerConfigHandler(s.svc) // Setup router with all trigger config routes s.router = s.setupRouter() } func (s *BotTriggerConfigHandlerTestSuite) setupRouter() *gin.Engine { r := gin.New() r.RedirectTrailingSlash = false r.Use(gin.Recovery()) // Account-level List (not in production router but handler supports it) r.GET("/api/v1/accounts/:account_id/trigger_configs", s.handler.List) // Bot-level routes (matches production router — no trailing slashes) botGroup := r.Group("/api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs") { botGroup.GET("", s.handler.ListByBot) botGroup.POST("", s.handler.Create) botGroup.GET("/:trigger_config_id", s.handler.Get) botGroup.PUT("/:trigger_config_id", s.handler.Update) botGroup.DELETE("/:trigger_config_id", s.handler.Delete) } // ToggleActive route (handler method exists but not in production router yet) r.PATCH("/api/v1/accounts/:account_id/agent_bots/:agent_bot_id/trigger_configs/:trigger_config_id/active", s.handler.ToggleActive) return r } func (s *BotTriggerConfigHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func (s *BotTriggerConfigHandlerTestSuite) SetupTest() { // Hard-delete all trigger configs to ensure clean state s.db.Exec("DELETE FROM bot_trigger_configs") } // ==================== List (account-level) ==================== func (s *BotTriggerConfigHandlerTestSuite) TestList_Empty() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.accountID), 10)+"/trigger_configs", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) triggerConfigs := resp["trigger_configs"] assert.NotNil(s.T(), triggerConfigs) meta := resp["meta"] metaMap, ok := meta.(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), float64(0), metaMap["count"]) } func (s *BotTriggerConfigHandlerTestSuite) TestList_WithConfigs() { // Seed two configs for the account s.createTestConfig("Config A", automation.BotRuleEventConversationCreated, true) s.createTestConfig("Config B", automation.BotRuleEventMessageCreated, true) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.accountID), 10)+"/trigger_configs", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) triggerConfigs := resp["trigger_configs"] tcList, ok := triggerConfigs.([]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), 2, len(tcList)) meta := resp["meta"] metaMap, ok := meta.(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), float64(2), metaMap["count"]) } func (s *BotTriggerConfigHandlerTestSuite) TestList_BadAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/trigger_configs", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } // ==================== ListByBot ==================== func (s *BotTriggerConfigHandlerTestSuite) TestListByBot_Empty() { w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) triggerConfigs := resp["trigger_configs"] assert.NotNil(s.T(), triggerConfigs) meta := resp["meta"] metaMap, ok := meta.(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), float64(0), metaMap["count"]) } func (s *BotTriggerConfigHandlerTestSuite) TestListByBot_WithConfigs() { s.createTestConfig("Config 1", automation.BotRuleEventConversationCreated, true) w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) triggerConfigs := resp["trigger_configs"] tcList, ok := triggerConfigs.([]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), 1, len(tcList)) } func (s *BotTriggerConfigHandlerTestSuite) TestListByBot_BadAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/agent_bots/1/trigger_configs", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } func (s *BotTriggerConfigHandlerTestSuite) TestListByBot_BadAgentBotID() { w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/abc/trigger_configs" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } // ==================== Get ==================== func (s *BotTriggerConfigHandlerTestSuite) TestGet_Success() { cfg := s.createTestConfig("GetTest Config", automation.BotRuleEventMessageCreated, true) w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), true, resp["success"]) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "GetTest Config", data["name"]) } func (s *BotTriggerConfigHandlerTestSuite) TestGet_NotFound() { w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/99999" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "NOT_FOUND", errorBody["code"]) } func (s *BotTriggerConfigHandlerTestSuite) TestGet_BadID() { w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/abc" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } // ==================== Create ==================== func (s *BotTriggerConfigHandlerTestSuite) TestCreate_Success() { body := `{ "name": "New Trigger Config", "event_name": "conversation_created", "conditions": [ {"attribute": "status", "filter_operator": "equal", "values": ["open"], "query_operator": "and"} ], "query_operator": "and", "active": true }` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusCreated, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), true, resp["success"]) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "New Trigger Config", data["name"]) assert.Equal(s.T(), "conversation_created", data["event_name"]) assert.NotNil(s.T(), data["id"]) } func (s *BotTriggerConfigHandlerTestSuite) TestCreate_DefaultsActiveAndQueryOperator() { // When JSON omits active and query_operator fields, the handler defaults: // - query_operator defaults to "and" (handler logic: if empty, set "and") // - active defaults to true (handler logic: if !config.Active && req.Active == false → set true) // NOTE: We must include conditions with valid values since ValidateConditions runs on Create. body := `{ "name": "Default Active Config", "event_name": "message_created", "conditions": [ {"attribute": "status", "filter_operator": "equal", "values": ["open"], "query_operator": "and"} ] }` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusCreated, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) // Default query_operator should be "and" assert.Equal(s.T(), "and", data["query_operator"]) // Default active should be true (handler defaults when not explicitly provided) assert.Equal(s.T(), true, data["active"]) } func (s *BotTriggerConfigHandlerTestSuite) TestCreate_BadAccountID() { body := `{"name": "Test", "event_name": "conversation_created"}` w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/agent_bots/1/trigger_configs", bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } func (s *BotTriggerConfigHandlerTestSuite) TestCreate_BadAgentBotID() { body := `{"name": "Test", "event_name": "conversation_created"}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/abc/trigger_configs" req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "BAD_REQUEST", errorBody["code"]) } func (s *BotTriggerConfigHandlerTestSuite) TestCreate_InvalidBody() { body := `{"name": "", "event_name": ""}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // ShouldBindJSON does NOT trigger validate:"required" tags, // so empty name/event_name will still bind successfully (just with zero values). // The Create call will go through to the service; GORM not-null constraint may fail. // Depending on SQLite behavior, this may result in 500 or 201 with zero values. // We verify the response is either a validation error or an internal error. // NOTE: SQLite doesn't enforce NOT NULL on string fields the same way as PG, // so empty strings may be accepted. This test documents the behavior. if w.Code == http.StatusCreated { // SQLite accepted empty strings — document this var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) // name and event_name will be empty strings assert.Equal(s.T(), "", data["name"]) } else { // Expected: bad request or validation error assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusUnprocessableEntity) } } func (s *BotTriggerConfigHandlerTestSuite) TestCreate_MalformedJSON() { body := `{invalid json` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs" req, _ := http.NewRequest("POST", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ==================== Update ==================== func (s *BotTriggerConfigHandlerTestSuite) TestUpdate_Success() { cfg := s.createTestConfig("Original Config", automation.BotRuleEventConversationCreated, true) body := `{ "name": "Updated Config Name", "description": "Updated description", "active": false }` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), true, resp["success"]) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "Updated Config Name", data["name"]) assert.Equal(s.T(), "Updated description", data["description"]) assert.Equal(s.T(), false, data["active"]) } func (s *BotTriggerConfigHandlerTestSuite) TestUpdate_PartialUpdate() { cfg := s.createTestConfig("Partial Update Config", automation.BotRuleEventMessageCreated, true) // Only update the name, leave other fields unchanged body := `{"name": "Partially Updated"}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "Partially Updated", data["name"]) // event_name should remain unchanged assert.Equal(s.T(), "message_created", data["event_name"]) } func (s *BotTriggerConfigHandlerTestSuite) TestUpdate_NotFound() { body := `{"name": "Update Nonexistent"}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/99999" req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *BotTriggerConfigHandlerTestSuite) TestUpdate_BadID() { body := `{"name": "Update Bad ID"}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/abc" req, _ := http.NewRequest("PUT", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ==================== Delete ==================== func (s *BotTriggerConfigHandlerTestSuite) TestDelete_Success() { cfg := s.createTestConfig("Delete Me", automation.BotRuleEventConversationCreated, true) w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) // response.NoContent → 204 assert.Equal(s.T(), http.StatusNoContent, w.Code) // Verify config is soft-deleted (GORM Delete with model.Base) _, err := s.svc.GetByID(context.Background(), cfg.ID) assert.Error(s.T(), err) // should be record not found } func (s *BotTriggerConfigHandlerTestSuite) TestDelete_NonExistentID() { // GORM Delete on non-existent ID returns nil → 204 w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/99999" req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNoContent, w.Code) } func (s *BotTriggerConfigHandlerTestSuite) TestDelete_BadID() { w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/abc" req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ==================== ToggleActive ==================== func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_Deactivate() { cfg := s.createTestConfig("Active Config", automation.BotRuleEventConversationCreated, true) // NOTE: The handler uses binding:"required" on the Active bool field. // Gin's binding:"required" validator for bool considers false as not satisfying "required", // so sending {"active": false} returns 400 VALIDATION_ERROR. // This is a known handler design quirk — "required" on bool means "must be true". // We test that deactivate with active=false fails with validation error. body := `{"active": false}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) + "/active" req, _ := http.NewRequest("PATCH", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Gin binding:"required" on bool rejects false → 400 validation error assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) errorBody, ok := resp["error"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), "VALIDATION_ERROR", errorBody["code"]) } func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_Activate() { cfg := s.createTestConfig("Inactive Config", automation.BotRuleEventMessageCreated, false) body := `{"active": true}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/" + strconv.FormatUint(uint64(cfg.ID), 10) + "/active" req, _ := http.NewRequest("PATCH", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &resp)) data, ok := resp["data"].(map[string]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), true, data["active"]) } func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_BadID() { body := `{"active": true}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/abc/active" req, _ := http.NewRequest("PATCH", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_NonExistentID() { // ToggleActive on non-existent ID → service does WHERE id = ? UPDATE, // which affects 0 rows but GORM doesn't return error → 200 with no-op body := `{"active": true}` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/99999/active" req, _ := http.NewRequest("PATCH", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // GORM UPDATE with WHERE on non-existent row returns nil error assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *BotTriggerConfigHandlerTestSuite) TestToggleActive_MalformedJSON() { body := `{bad json` w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(s.accountID), 10) + "/agent_bots/" + strconv.FormatUint(uint64(s.agentBotID), 10) + "/trigger_configs/1/active" req, _ := http.NewRequest("PATCH", url, bytes.NewBufferString(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ==================== Helper ==================== // createTestConfig creates a BotTriggerConfig directly via the service for test seeding. func (s *BotTriggerConfigHandlerTestSuite) createTestConfig(name string, eventName automation.BotRuleEventType, active bool) *automation.BotTriggerConfig { config := &automation.BotTriggerConfig{ AccountID: s.accountID, AgentBotID: s.agentBotID, Name: name, EventName: eventName, Conditions: automation.TriggerConditions{ {Attribute: "status", FilterOperator: "equal", Values: []string{"open"}, QueryOperator: "and"}, }, QueryOperator: "and", Active: active, } err := s.svc.Create(context.Background(), config) s.Require().NoError(err) return config }