package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) type LabelHandlerTestSuite struct { suite.Suite db *gorm.DB handler *LabelHandler account *model.Account } func (s *LabelHandlerTestSuite) 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.Tag{}, &model.ConversationLabel{}, &model.ContactLabel{}, &model.Conversation{}, &model.Inbox{}, &model.Contact{})) s.db = db convLabelRepo := repository.NewConversationLabelRepo(db) tagRepo := repository.NewTagRepo(db) tagSvc := service.NewTagService(tagRepo) labelSvc := service.NewLabelService(convLabelRepo, tagRepo) s.handler = NewLabelHandler(tagSvc, labelSvc) s.account = &model.Account{Name: "test-label-account"} s.Require().NoError(db.Create(s.account).Error) } func (s *LabelHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func TestLabelHandlerSuite(t *testing.T) { suite.Run(t, new(LabelHandlerTestSuite)) } func labelBoolPtr(v bool) *bool { return &v } func (s *LabelHandlerTestSuite) SetupTest() { s.Require().NoError(s.db.Exec("DELETE FROM conversation_labels").Error) s.Require().NoError(s.db.Exec("DELETE FROM contact_labels").Error) s.Require().NoError(s.db.Unscoped().Where("account_id = ?", s.account.ID).Delete(&model.Tag{}).Error) } func (s *LabelHandlerTestSuite) TestListTags_Success() { r := gin.New() r.GET("/api/v1/accounts/:account_id/tags", s.handler.ListTags) s.Require().NoError(s.db.Create(&model.Tag{AccountID: s.account.ID, Name: "billing", Color: "#ff0000", Description: "Billing issues", ShowOnSidebar: labelBoolPtr(true)}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/tags", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string][]map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) s.Require().Len(resp["payload"], 1) assert.Equal(s.T(), "billing", resp["payload"][0]["title"]) assert.Equal(s.T(), "Billing issues", resp["payload"][0]["description"]) assert.Equal(s.T(), "#ff0000", resp["payload"][0]["color"]) assert.Equal(s.T(), true, resp["payload"][0]["show_on_sidebar"]) } func (s *LabelHandlerTestSuite) TestCreateTag_BadRequest_EmptyBody() { r := gin.New() r.POST("/api/v1/accounts/:account_id/tags", s.handler.CreateTag) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/tags", 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 *LabelHandlerTestSuite) TestCreateTag_Success() { r := gin.New() r.POST("/api/v1/accounts/:account_id/labels", s.handler.CreateTag) body := map[string]interface{}{"label": map[string]interface{}{"title": "Priority", "description": "Hot queue", "color": "#FF0000", "show_on_sidebar": false}} b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/labels", 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)) assert.Equal(s.T(), "priority", resp["title"]) assert.Equal(s.T(), "Hot queue", resp["description"]) assert.Equal(s.T(), "#FF0000", resp["color"]) assert.Equal(s.T(), false, resp["show_on_sidebar"]) } func (s *LabelHandlerTestSuite) TestCreateTag_RawNameCompatibility() { r := gin.New() r.POST("/api/v1/accounts/:account_id/tags", s.handler.CreateTag) body := map[string]interface{}{"name": "legacy", "color": ""} b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/tags", 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)) assert.Equal(s.T(), "legacy", resp["title"]) assert.Equal(s.T(), "#1f93ff", resp["color"]) assert.Equal(s.T(), true, resp["show_on_sidebar"]) } func (s *LabelHandlerTestSuite) TestUpdateTag_ChatwootPayloadAndAccountScope() { r := gin.New() r.PUT("/api/v1/accounts/:account_id/labels/:tag_id", s.handler.UpdateTag) tag := &model.Tag{AccountID: s.account.ID, Name: "old", Color: "#000000", ShowOnSidebar: labelBoolPtr(true)} s.Require().NoError(s.db.Create(tag).Error) desc := "New description" color := "#00ff00" show := false body := map[string]interface{}{"label": map[string]interface{}{"title": "New", "description": desc, "color": color, "show_on_sidebar": show}} b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/labels/%d", s.account.ID, tag.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)) assert.Equal(s.T(), "new", resp["title"]) assert.Equal(s.T(), desc, resp["description"]) assert.Equal(s.T(), color, resp["color"]) assert.Equal(s.T(), show, resp["show_on_sidebar"]) } func (s *LabelHandlerTestSuite) TestDeleteTag_ReturnsOKAndRemovesAssociations() { r := gin.New() r.DELETE("/api/v1/accounts/:account_id/labels/:tag_id", s.handler.DeleteTag) tag := &model.Tag{AccountID: s.account.ID, Name: "old", Color: "#000000", ShowOnSidebar: labelBoolPtr(true)} s.Require().NoError(s.db.Create(tag).Error) s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.account.ID, ConversationID: 10, TagID: tag.ID}).Error) s.Require().NoError(s.db.Create(&model.ContactLabel{AccountID: s.account.ID, ContactID: 20, TagID: tag.ID}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/labels/%d", s.account.ID, tag.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var convCount int64 s.Require().NoError(s.db.Model(&model.ConversationLabel{}).Where("tag_id = ?", tag.ID).Count(&convCount).Error) assert.Equal(s.T(), int64(0), convCount) var contactCount int64 s.Require().NoError(s.db.Model(&model.ContactLabel{}).Where("tag_id = ?", tag.ID).Count(&contactCount).Error) assert.Equal(s.T(), int64(0), contactCount) } func (s *LabelHandlerTestSuite) TestGetTag_BadRequest_InvalidID() { r := gin.New() r.GET("/api/v1/accounts/:account_id/tags/:tag_id", s.handler.GetTag) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/tags/abc", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *LabelHandlerTestSuite) TestGetTag_NotFound() { r := gin.New() r.GET("/api/v1/accounts/:account_id/tags/:tag_id", s.handler.GetTag) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/tags/99999", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *LabelHandlerTestSuite) TestUpdateTag_BadRequest_InvalidID() { r := gin.New() r.PUT("/api/v1/accounts/:account_id/tags/:tag_id", s.handler.UpdateTag) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", fmt.Sprintf("/api/v1/accounts/%d/tags/abc", 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 *LabelHandlerTestSuite) TestDeleteTag_BadRequest_InvalidID() { r := gin.New() r.DELETE("/api/v1/accounts/:account_id/tags/:tag_id", s.handler.DeleteTag) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/tags/abc", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *LabelHandlerTestSuite) TestAddLabelToConversation_BadRequest_InvalidConvID() { r := gin.New() r.POST("/api/v1/accounts/:account_id/conversations/:conversation_id/labels", s.handler.AddLabelToConversation) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/conversations/abc/labels", 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 *LabelHandlerTestSuite) TestRemoveLabelFromConversation_BadRequest_InvalidIDs() { r := gin.New() r.DELETE("/api/v1/accounts/:account_id/conversations/:conversation_id/labels/:tag_id", s.handler.RemoveLabelFromConversation) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/conversations/abc/labels/1", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *LabelHandlerTestSuite) TestGetConversationLabels_BadRequest_InvalidConvID() { r := gin.New() r.GET("/api/v1/accounts/:account_id/conversations/:conversation_id/labels", s.handler.GetConversationLabels) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", fmt.Sprintf("/api/v1/accounts/%d/conversations/abc/labels", s.account.ID), nil) r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *LabelHandlerTestSuite) TestBatchAddLabel_BadRequest_InvalidAccountID() { r := gin.New() r.POST("/api/v1/accounts/:account_id/conversations/batch_labels/add", s.handler.BatchAddLabel) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations/batch_labels/add", nil) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *LabelHandlerTestSuite) TestBatchRemoveLabel_BadRequest_InvalidAccountID() { r := gin.New() r.POST("/api/v1/accounts/:account_id/conversations/batch_labels/remove", s.handler.BatchRemoveLabel) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations/batch_labels/remove", nil) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) }