package v1 import ( "bytes" "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/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // --- ConversationParticipant Handler Test Suite --- type ConversationParticipantHandlerTestSuite struct { suite.Suite router *gin.Engine db *gorm.DB testAccount *model.Account testUser *model.User testConv *model.Conversation } func (s *ConversationParticipantHandlerTestSuite) SetupSuite() { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) s.Require().NoError(err) s.db = db err = db.AutoMigrate( &model.Account{}, &model.User{}, &model.Inbox{}, &model.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.ConversationParticipant{}, ) s.Require().NoError(err) // Create test account account := &model.Account{Name: "ParticipantHandlerOrg", Locale: "en", Active: true} s.Require().NoError(db.Create(account).Error) s.testAccount = account // Create test user user := &model.User{Name: "ParticipantHandlerUser", Email: "participant-handler@test.com", Password: "hashed", Role: "agent", Active: true} s.Require().NoError(db.Create(user).Error) s.testUser = user // Create inbox and contact inbox := &model.Inbox{AccountID: account.ID, Name: "ParticipantHandlerInbox", ChannelType: "web_widget", ChannelID: 1} s.Require().NoError(db.Create(inbox).Error) contact := &model.Contact{AccountID: account.ID, Name: "ParticipantHandlerContact"} s.Require().NoError(db.Create(contact).Error) // Create conversation conv := &model.Conversation{AccountID: account.ID, InboxID: inbox.ID, ContactID: contact.ID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(db.Create(conv).Error) s.testConv = conv // Wire up repos, services, handlers convRepo := repository.NewConversationRepo(db) participantRepo := repository.NewConversationParticipantRepo(db) participantSvc := service.NewConversationParticipantService(participantRepo, convRepo) handler := NewConversationParticipantHandler(participantSvc) // Setup router r := gin.New() s.router = r // Register routes accountGroup := r.Group("/api/v1/accounts/:account_id") { convGroup := accountGroup.Group("/conversations/:conversation_id") { participants := convGroup.Group("/participants") { participants.GET("", handler.List) participants.GET("/", handler.List) participants.POST("", handler.Add) participants.POST("/", handler.Add) participants.PATCH("", handler.BatchUpdate) participants.PATCH("/", handler.BatchUpdate) participants.PUT("", handler.BatchUpdate) participants.DELETE("", handler.Destroy) participants.PATCH("/:user_id", handler.Update) participants.DELETE("/:user_id", handler.Remove) } } } } func (s *ConversationParticipantHandlerTestSuite) Test_AddParticipant() { body := map[string]interface{}{ "user_ids": []uint{s.testUser.ID}, } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(b)) 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.Len(s.T(), resp, 1) assert.Equal(s.T(), float64(s.testUser.ID), resp[0]["id"]) assert.Equal(s.T(), s.testUser.Email, resp[0]["email"]) assert.NotContains(s.T(), resp[0], "conversation_id") assert.NotContains(s.T(), resp[0], "user_id") } func (s *ConversationParticipantHandlerTestSuite) Test_ListParticipants() { // First add a participant body := map[string]interface{}{ "user_id": s.testUser.ID, "role": "participant", } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Now list w = httptest.NewRecorder() req, _ = http.NewRequest("GET", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", 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.NotEmpty(s.T(), resp) assert.NotContains(s.T(), resp[0], "data") } func (s *ConversationParticipantHandlerTestSuite) Test_RemoveParticipant() { // First add a participant body := map[string]interface{}{ "user_id": s.testUser.ID, "role": "participant", } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Now remove w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants/"+strconv.FormatUint(uint64(s.testUser.ID), 10), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *ConversationParticipantHandlerTestSuite) Test_UpdateParticipantRole() { // First add a participant body := map[string]interface{}{ "user_id": s.testUser.ID, "role": "participant", } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Now update role body = map[string]interface{}{ "role": "assignee", } b, _ = json.Marshal(body) w = httptest.NewRecorder() req, _ = http.NewRequest("PATCH", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/participants/"+strconv.FormatUint(uint64(s.testUser.ID), 10), bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *ConversationParticipantHandlerTestSuite) Test_AddParticipant_InvalidAccountID() { body := map[string]interface{}{ "user_id": 1, "role": "participant", } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/1/participants", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ========== BatchUpdate Handler Tests ========== func (s *ConversationParticipantHandlerTestSuite) Test_BatchUpdate_AddAndRemove() { user1 := &model.User{Name: "BatchUser1", Email: "batch1@test.com", Password: "hashed", Role: "agent", Active: true} s.Require().NoError(s.db.Create(user1).Error) user2 := &model.User{Name: "BatchUser2", Email: "batch2@test.com", Password: "hashed", Role: "agent", Active: true} s.Require().NoError(s.db.Create(user2).Error) conv := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testConv.InboxID, ContactID: s.testConv.ContactID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(conv).Error) // First add user1 as participant. addBody := map[string]interface{}{ "user_ids": []uint{user1.ID}, } addBytes, _ := json.Marshal(addBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(addBytes)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) // Chatwoot update treats user_ids as the final participant set. batchBody := map[string]interface{}{ "user_ids": []uint{user2.ID}, } batchBytes, _ := json.Marshal(batchBody) w = httptest.NewRecorder() req, _ = http.NewRequest("PATCH", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(batchBytes)) 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.Len(s.T(), resp, 1) assert.Equal(s.T(), float64(user2.ID), resp[0]["id"]) var count int64 s.Require().NoError(s.db.Model(&model.ConversationParticipant{}).Where("conversation_id = ? AND user_id = ?", conv.ID, user1.ID).Count(&count).Error) assert.Equal(s.T(), int64(0), count) } func (s *ConversationParticipantHandlerTestSuite) Test_DestroyParticipantsRawPayload() { user := &model.User{Name: "DestroyUser", Email: "destroy-participant@test.com", Password: "hashed", Role: "agent", Active: true} s.Require().NoError(s.db.Create(user).Error) conv := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testConv.InboxID, ContactID: s.testConv.ContactID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(conv).Error) s.Require().NoError(s.db.Create(&model.ConversationParticipant{AccountID: s.testAccount.ID, ConversationID: conv.ID, UserID: user.ID}).Error) body, _ := json.Marshal(map[string]interface{}{"user_ids": []uint{user.ID}}) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+strconv.FormatUint(uint64(s.testAccount.ID), 10)+"/conversations/"+strconv.FormatUint(uint64(conv.ID), 10)+"/participants", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) assert.Empty(s.T(), w.Body.String()) } func (s *ConversationParticipantHandlerTestSuite) Test_BatchUpdate_InvalidAccountID() { batchBody := map[string]interface{}{ "user_ids": []uint{1}, } batchBytes, _ := json.Marshal(batchBody) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/invalid/conversations/1/participants", bytes.NewReader(batchBytes)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func TestConversationParticipantHandlerTestSuite(t *testing.T) { suite.Run(t, new(ConversationParticipantHandlerTestSuite)) }