package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/automation" "github.com/gochat/gochat/internal/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) type testMacroDBProvider struct { db *gorm.DB } func (p *testMacroDBProvider) DB() *gorm.DB { return p.db } type MacroHandlerTestSuite struct { suite.Suite db *gorm.DB handler *MacroHandler account *model.Account user *model.User } func (s *MacroHandlerTestSuite) 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) s.Require().NoError(db.AutoMigrate( &model.Account{}, &model.User{}, &model.AccountUser{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.Message{}, &automation.Macro{}, &automation.MacroExecution{}, &automation.ConversationLabel{}, &automation.ConversationMute{}, )) s.db = db macroSvc := automation.NewMacroService(&testMacroDBProvider{db: db}) s.handler = NewMacroHandler(macroSvc) s.account = &model.Account{Name: "test-macro-account"} s.db.Create(s.account) s.user = &model.User{Name: "test-macro-user", Email: "macro@example.com"} s.db.Create(s.user) } func (s *MacroHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func TestMacroHandlerSuite(t *testing.T) { suite.Run(t, new(MacroHandlerTestSuite)) } func (s *MacroHandlerTestSuite) TestList_Empty() { r := gin.New() r.GET("/api/v1/accounts/:account_id/macros", s.handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/macros", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *MacroHandlerTestSuite) TestCreate_Success() { r := gin.New() r.POST("/api/v1/accounts/:account_id/macros", func(c *gin.Context) { c.Set("user_id", float64(s.user.ID)) c.Next() }, s.handler.Create) body := map[string]interface{}{ "name": "test-macro", "visibility": "global", "actions": []map[string]interface{}{{"action_name": "assign_agent", "action_params": []interface{}{"self"}}}, } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/macros", s.account.ID), bytes.NewBuffer(b)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) payload := resp["payload"].(map[string]interface{}) assert.Equal(s.T(), "test-macro", payload["name"]) assert.Equal(s.T(), "global", payload["visibility"]) createdBy := payload["created_by"].(map[string]interface{}) assert.Equal(s.T(), float64(s.user.ID), createdBy["id"]) actions := payload["actions"].([]interface{}) action := actions[0].(map[string]interface{}) assert.Equal(s.T(), []interface{}{"self"}, action["action_params"]) } func (s *MacroHandlerTestSuite) TestCreate_BadRequest() { r := gin.New() r.POST("/api/v1/accounts/:account_id/macros", s.handler.Create) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/macros", s.account.ID), nil) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *MacroHandlerTestSuite) TestUpdateDeleteAuthorizationAndPayload() { admin := &model.User{Name: "Admin", Email: "macro-admin@example.com", Role: "administrator"} s.Require().NoError(s.db.Create(admin).Error) agent := &model.User{Name: "Agent", Email: "macro-agent@example.com", Role: "agent"} s.Require().NoError(s.db.Create(agent).Error) macro := &automation.Macro{ AccountID: s.account.ID, Name: "public macro", Actions: automation.Actions{}, Visibility: automation.MacroVisibilityGlobal, Active: true, CreatedByID: admin.ID, UpdatedByID: admin.ID, } s.Require().NoError(s.db.Create(macro).Error) r := gin.New() r.PUT("/api/v1/accounts/:account_id/macros/:macro_id", func(c *gin.Context) { c.Set("user_id", agent.ID) c.Set("role", "agent") c.Next() }, s.handler.Update) body := []byte(`{"name":"agent edit","visibility":"global","actions":[]}`) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/macros/%d", s.account.ID, macro.ID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusUnauthorized, w.Code) r = gin.New() r.PUT("/api/v1/accounts/:account_id/macros/:macro_id", func(c *gin.Context) { c.Set("user_id", admin.ID) c.Set("role", "administrator") c.Next() }, s.handler.Update) w = httptest.NewRecorder() req, _ = http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/macros/%d", s.account.ID, macro.ID), bytes.NewBuffer(body)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) payload := resp["payload"].(map[string]interface{}) assert.Equal(s.T(), "agent edit", payload["name"]) assert.Equal(s.T(), "global", payload["visibility"]) } func (s *MacroHandlerTestSuite) TestExecute_UsesConversationDisplayIDsAndMutatesConversation() { admin := &model.User{Name: "Exec Admin", Email: "macro-exec-admin@example.com", Role: "administrator"} s.Require().NoError(s.db.Create(admin).Error) inbox := &model.Inbox{AccountID: s.account.ID, Name: "Macro Inbox", ChannelType: "web"} s.Require().NoError(s.db.Create(inbox).Error) contact := &model.Contact{AccountID: s.account.ID, Name: "Macro Contact", Email: "macro-contact@example.com"} s.Require().NoError(s.db.Create(contact).Error) displayID := uint(444) conversation := &model.Conversation{AccountID: s.account.ID, InboxID: inbox.ID, ContactID: contact.ID, DisplayID: &displayID, Status: "open", Priority: "low", ChannelType: "web", Channel: "web_widget"} s.Require().NoError(s.db.Create(conversation).Error) macro := &automation.Macro{ AccountID: s.account.ID, Name: "execute macro", Actions: automation.Actions{ {ActionName: "add_label", ActionParams: map[string]interface{}{"labels": []string{"vip"}}}, {ActionName: "change_status", ActionParams: map[string]interface{}{"status": "resolved"}}, {ActionName: "send_message", ActionParams: map[string]interface{}{"content": "Hello from macro"}}, {ActionName: "add_private_note", ActionParams: map[string]interface{}{"content": "Internal macro note"}}, }, Visibility: automation.MacroVisibilityGlobal, Active: true, CreatedByID: admin.ID, UpdatedByID: admin.ID, } s.Require().NoError(s.db.Create(macro).Error) r := gin.New() r.POST("/api/v1/accounts/:account_id/macros/:macro_id/execute", func(c *gin.Context) { c.Set("user_id", admin.ID) c.Set("role", "administrator") c.Next() }, s.handler.Execute) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/macros/%d/execute", s.account.ID, macro.ID), bytes.NewBufferString(`{"conversation_ids":[444]}`)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) assert.Empty(s.T(), w.Body.String()) var reloaded model.Conversation s.Require().NoError(s.db.First(&reloaded, conversation.ID).Error) assert.Equal(s.T(), "resolved", reloaded.Status) var label automation.ConversationLabel s.Require().NoError(s.db.Where("conversation_id = ? AND label = ?", conversation.ID, "vip").First(&label).Error) var messages []model.Message s.Require().NoError(s.db.Where("conversation_id = ?", conversation.ID).Order("id ASC").Find(&messages).Error) s.Require().Len(messages, 2) assert.Equal(s.T(), "Hello from macro", messages[0].Content) assert.Equal(s.T(), admin.ID, *messages[0].SenderID) assert.False(s.T(), messages[0].Private) assert.Equal(s.T(), "Internal macro note", messages[1].Content) assert.True(s.T(), messages[1].Private) }