package v1 import ( "bytes" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/channel" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // --- Conversation Handler Test Suite --- type ConversationHandlerTestSuite struct { suite.Suite router *gin.Engine handler *ConversationHandler db *gorm.DB } func (s *ConversationHandlerTestSuite) SetupSuite() { gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) s.Require().NoError(err) s.db = db // AutoMigrate all required models err = db.AutoMigrate( &model.Account{}, &model.Inbox{}, &model.Contact{}, &model.Conversation{}, &model.Message{}, ) s.Require().NoError(err) // Create repositories and services convRepo := repository.NewConversationRepo(db) msgRepo := repository.NewMessageRepo(db) dispatcher := channel.NewDispatcher() convSvc := service.NewConversationService(convRepo, msgRepo, dispatcher) msgSvc := service.NewMessageService(msgRepo, dispatcher) // Create handler s.handler = NewConversationHandler(convSvc, msgSvc) // Setup router with all conversation routes s.router = gin.New() s.router.GET("/api/v1/accounts/:account_id/conversations", s.handler.List) s.router.POST("/api/v1/accounts/:account_id/conversations", s.handler.Create) s.router.GET("/api/v1/accounts/:account_id/conversations/:id", s.handler.Get) s.router.PATCH("/api/v1/accounts/:account_id/conversations/:id", s.handler.Update) s.router.POST("/api/v1/accounts/:account_id/conversations/:id/assign", s.handler.AssignAgent) s.router.POST("/api/v1/accounts/:account_id/conversations/:id/toggle_status", s.handler.ToggleStatus) s.router.PATCH("/api/v1/accounts/:account_id/conversations/:id/labels", s.handler.UpdateLabels) s.router.GET("/api/v1/accounts/:account_id/conversations/:id/messages", s.handler.ListMessages) } func (s *ConversationHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func (s *ConversationHandlerTestSuite) SetupTest() { // Seed data for each test s.db.Create(&model.Account{Name: "Test Account"}) s.db.Create(&model.Inbox{AccountID: 1, Name: "Test Inbox", ChannelType: "web_widget", ChannelID: 1}) s.db.Create(&model.Contact{AccountID: 1, Name: "Test Contact"}) s.db.Create(&model.Conversation{ AccountID: 1, InboxID: 1, ContactID: 1, Status: string(model.ConversationStatusOpen), Priority: string(model.ConversationPriorityMedium), ChannelType: "web_widget", Channel: "web_widget", }) } func (s *ConversationHandlerTestSuite) TearDownTest() { s.db.Exec("DELETE FROM messages") s.db.Exec("DELETE FROM conversations") s.db.Exec("DELETE FROM contacts") s.db.Exec("DELETE FROM inboxes") s.db.Exec("DELETE FROM accounts") // Reset auto-increment counters so IDs start from 1 again s.db.Exec("DELETE FROM sqlite_sequence WHERE name IN ('messages','conversations','contacts','inboxes','accounts')") } func TestConversationHandlerTestSuite(t *testing.T) { suite.Run(t, new(ConversationHandlerTestSuite)) } // --- Test Cases --- func (s *ConversationHandlerTestSuite) TestList_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) // Data should be an array data, ok := resp["data"].([]interface{}) assert.True(s.T(), ok) assert.GreaterOrEqual(s.T(), len(data), 1) } func (s *ConversationHandlerTestSuite) TestList_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/conversations", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.False(s.T(), resp["success"].(bool)) } func (s *ConversationHandlerTestSuite) TestList_WithStatusFilter() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations?status=open", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *ConversationHandlerTestSuite) TestGet_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/1", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), float64(1), data["id"]) } func (s *ConversationHandlerTestSuite) TestGet_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/999", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationHandlerTestSuite) TestGet_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/abc", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestCreate_Success() { payload := map[string]interface{}{ "inbox_id": 1, "contact_id": 1, "status": "open", "priority": "medium", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", bytes.NewReader(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{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) data := resp["data"].(map[string]interface{}) assert.NotNil(s.T(), data["id"]) } func (s *ConversationHandlerTestSuite) TestCreate_InvalidAccountID() { payload := map[string]interface{}{ "inbox_id": 1, "contact_id": 1, } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/conversations", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestCreate_MissingRequiredFields() { payload := map[string]interface{}{} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Should fail validation (missing inbox_id, contact_id) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestUpdate_Success() { payload := map[string]interface{}{ "status": "resolved", "priority": "high", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/1", bytes.NewReader(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{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "resolved", data["status"]) assert.Equal(s.T(), "high", data["priority"]) } func (s *ConversationHandlerTestSuite) TestUpdate_NotFound() { payload := map[string]interface{}{ "status": "resolved", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/999", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationHandlerTestSuite) TestAssignAgent_Success() { payload := map[string]interface{}{ "assignee_id": 42, } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/1/assign", bytes.NewReader(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{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].(map[string]interface{}) assert.NotNil(s.T(), data["assignee_id"]) } func (s *ConversationHandlerTestSuite) TestAssignAgent_NotFound() { payload := map[string]interface{}{ "assignee_id": 42, } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/999/assign", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationHandlerTestSuite) TestAssignAgent_MissingAssigneeID() { payload := map[string]interface{}{} body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/1/assign", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestToggleStatus_Success() { payload := map[string]interface{}{ "status": "resolved", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/1/toggle_status", bytes.NewReader(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{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].(map[string]interface{}) assert.Equal(s.T(), "resolved", data["status"]) } func (s *ConversationHandlerTestSuite) TestToggleStatus_InvalidStatus() { payload := map[string]interface{}{ "status": "invalid_status", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/1/toggle_status", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestToggleStatus_NotFound() { payload := map[string]interface{}{ "status": "resolved", } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/conversations/999/toggle_status", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationHandlerTestSuite) TestUpdateLabels_Success() { payload := map[string]interface{}{ "labels": []string{"bug", "urgent"}, } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/1/labels", bytes.NewReader(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{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) } func (s *ConversationHandlerTestSuite) TestUpdateLabels_NotFound() { payload := map[string]interface{}{ "labels": []string{"bug"}, } body, _ := json.Marshal(payload) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", "/api/v1/accounts/1/conversations/999/labels", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationHandlerTestSuite) TestListMessages_Success() { // Create a message in the conversation s.db.Create(&model.Message{ ConversationID: 1, AccountID: 1, InboxID: 1, Content: "Hello world", ContentType: "text", MessageType: "outgoing", }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/1/messages", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.True(s.T(), resp["success"].(bool)) data, ok := resp["data"].([]interface{}) assert.True(s.T(), ok) assert.GreaterOrEqual(s.T(), len(data), 1) } func (s *ConversationHandlerTestSuite) TestListMessages_InvalidConversationID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/abc/messages", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationHandlerTestSuite) TestListMessages_Empty() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/conversations/1/messages", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) data, ok := resp["data"].([]interface{}) assert.True(s.T(), ok) assert.Equal(s.T(), 0, len(data)) } func TestHandleServiceError_NilError(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) handleServiceError(c, nil) assert.False(t, c.IsAborted()) } func TestHandleServiceError_NotFoundMessage(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = &http.Request{Header: http.Header{}} handleServiceError(c, errors.New("record not found")) assert.True(t, c.IsAborted()) } func TestHandleServiceError_InvalidMessage(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = &http.Request{Header: http.Header{}} handleServiceError(c, errors.New("invalid input")) assert.True(t, c.IsAborted()) } func TestHandleServiceError_GenericError(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = &http.Request{Header: http.Header{}} handleServiceError(c, errors.New("something went wrong")) assert.True(t, c.IsAborted()) }