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/channel" "github.com/gochat/gochat/internal/llm" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // --- Mock LLM Provider for CRUD handler tests --- // (Duplicate type name avoided by using a different name from conversation_handler_test.go) type mockConvCrudLLMProvider struct{} func (m *mockConvCrudLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatRequest) (*llm.ChatResponse, error) { return &llm.ChatResponse{}, nil } func (m *mockConvCrudLLMProvider) CreateEmbedding(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) { return &llm.EmbeddingResponse{}, nil } func (m *mockConvCrudLLMProvider) ChatCompletionStream(ctx context.Context, req llm.ChatRequest, onChunk func(llm.StreamChunk) error) error { return nil } // skipIfSQLiteForConv skips tests that require PostgreSQL-specific features (ILIKE, pg_trgm). func skipIfSQLiteForConv(t *testing.T) { t.Helper() t.Skip("Skipping: this test requires PostgreSQL (ILIKE / trigram)") } // --- Conversation CRUD Handler Test Suite --- type ConversationCrudTestSuite struct { suite.Suite router *gin.Engine db *gorm.DB handler *ConversationHandler testAccount *model.Account testInbox *model.Inbox testContact *model.Contact testConv *model.Conversation } func (s *ConversationCrudTestSuite) 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{}, &model.Message{}, &model.InboxMember{}, &model.Tag{}, &model.ConversationLabel{}, &model.AccountUser{}, &model.Team{}, &model.TeamMember{}, &model.SlaPolicy{}, &model.AppliedSLA{}, &model.SlaEvent{}, &model.CaptainAssistant{}, &model.CaptainInbox{}, ) s.Require().NoError(err) // Wire up repos, services, handlers convRepo := repository.NewConversationRepo(db) msgRepo := repository.NewMessageRepo(db) dispatcher := channel.NewDispatcher() inboxMemberRepo := repository.NewInboxMemberRepo(db) inboxMemberSvc := service.NewInboxMemberService(inboxMemberRepo) accountUserRepo := repository.NewAccountUserRepo(db) teamRepo := repository.NewTeamRepo(db) teamMemberRepo := repository.NewTeamMemberRepo(db) conversationSvc := service.NewConversationService(convRepo, msgRepo, dispatcher, inboxMemberSvc, accountUserRepo, teamRepo, teamMemberRepo) appliedSlaSvc := service.NewAppliedSlaService(repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyRepo(db), convRepo) conversationSvc.SetAppliedSlaService(appliedSlaSvc) mockLLM := &mockConvCrudLLMProvider{} messageSvc := service.NewMessageService(msgRepo, dispatcher, mockLLM) handler := NewConversationHandler(conversationSvc, messageSvc) s.handler = handler // Setup router r := gin.New() s.router = r accountGroup := r.Group("/api/v1/accounts/:account_id") { conversations := accountGroup.Group("/conversations") { conversations.GET("", handler.List) conversations.POST("", handler.Create) conversations.GET("/:conversation_id", handler.Get) conversations.PUT("/:conversation_id", handler.Update) conversations.DELETE("/:conversation_id", handler.Delete) conversations.POST("/:conversation_id/assign", handler.AssignAgent) conversations.POST("/:conversation_id/toggle_status", handler.ToggleStatus) conversations.POST("/:conversation_id/mute", handler.Mute) conversations.POST("/:conversation_id/unmute", handler.Unmute) conversations.POST("/:conversation_id/labels", handler.UpdateLabels) conversations.GET("/search", handler.Search) conversations.POST("/filter", handler.Filter) conversations.POST("/:conversation_id/priority", handler.UpdatePriority) conversations.POST("/:conversation_id/assignments", handler.AssignTeam) conversations.GET("/:conversation_id/inbox_assistant", handler.InboxAssistant) } } } func (s *ConversationCrudTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func (s *ConversationCrudTestSuite) SetupTest() { // Create test account account := &model.Account{Name: "CrudTestOrg", Locale: "en", Active: true} s.Require().NoError(s.db.Create(account).Error) s.testAccount = account // Create inbox inbox := &model.Inbox{AccountID: account.ID, Name: "CrudTestInbox", ChannelType: "web_widget", ChannelID: 1} s.Require().NoError(s.db.Create(inbox).Error) s.testInbox = inbox // Create contact contact := &model.Contact{AccountID: account.ID, Name: "CrudTestContact"} s.Require().NoError(s.db.Create(contact).Error) s.testContact = contact // 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(s.db.Create(conv).Error) s.testConv = conv } func (s *ConversationCrudTestSuite) TearDownTest() { s.db.Exec("DELETE FROM sla_events") s.db.Exec("DELETE FROM applied_slas") s.db.Exec("DELETE FROM conversation_labels") s.db.Exec("DELETE FROM conversations") s.db.Exec("DELETE FROM contact_inboxes") s.db.Exec("DELETE FROM contacts") s.db.Exec("DELETE FROM inbox_members") s.db.Exec("DELETE FROM inboxes") s.db.Exec("DELETE FROM messages") s.db.Exec("DELETE FROM account_users") s.db.Exec("DELETE FROM users") s.db.Exec("DELETE FROM accounts") s.db.Exec("DELETE FROM teams") s.db.Exec("DELETE FROM team_members") s.db.Exec("DELETE FROM sla_policies") s.db.Exec("DELETE FROM captain_inboxes") s.db.Exec("DELETE FROM captain_assistants") } func (s *ConversationCrudTestSuite) accountURL() string { return "/api/v1/accounts/" + strconv.FormatUint(uint64(s.testAccount.ID), 10) } func (s *ConversationCrudTestSuite) convURL(id uint) string { return s.accountURL() + "/conversations/" + strconv.FormatUint(uint64(id), 10) } // ========== List Handler Tests ========== func (s *ConversationCrudTestSuite) TestList_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Data struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]interface{} `json:"payload"` } `json:"data"` } err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount) assert.Len(s.T(), resp.Data.Payload, 1) assert.NotNil(s.T(), resp.Data.Payload[0]["meta"]) } func (s *ConversationCrudTestSuite) TestList_WithStatusFilter() { resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(resolved).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations?status=open", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Data struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` } `json:"data"` } err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount) } func (s *ConversationCrudTestSuite) TestList_DefaultsToOpenStatus() { resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(resolved).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Data struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]interface{} `json:"payload"` } `json:"data"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount) assert.Len(s.T(), resp.Data.Payload, 1) } func (s *ConversationCrudTestSuite) TestList_AppliesFinderFiltersAfterCounts() { assigneeID := uint(42) assigned := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, AssigneeID: &assigneeID, Status: "open", Labels: "vip", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(assigned).Error) unmatched := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "open", Labels: "standard", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(unmatched).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations?labels=vip&assignee_type=assigned", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Data struct { Meta struct { AssignedCount int64 `json:"assigned_count"` UnassignedCount int64 `json:"unassigned_count"` AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]interface{} `json:"payload"` } `json:"data"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), int64(1), resp.Data.Meta.AllCount) assert.Equal(s.T(), int64(1), resp.Data.Meta.AssignedCount) assert.Equal(s.T(), int64(0), resp.Data.Meta.UnassignedCount) assert.Len(s.T(), resp.Data.Payload, 1) } func (s *ConversationCrudTestSuite) TestList_QuerySkipsStatusFilter() { resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(resolved).Error) s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error) s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations?q=needle&status=open", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Data struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]interface{} `json:"payload"` } `json:"data"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), int64(2), resp.Data.Meta.AllCount) assert.Len(s.T(), resp.Data.Payload, 2) } func (s *ConversationCrudTestSuite) TestList_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/invalid/conversations", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ========== Create Handler Tests ========== func (s *ConversationCrudTestSuite) TestCreate_Success() { body, _ := json.Marshal(map[string]interface{}{ "inbox_id": s.testInbox.ID, "contact_id": s.testContact.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations", 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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.NotNil(s.T(), resp["id"]) assert.NotNil(s.T(), resp["meta"]) } func (s *ConversationCrudTestSuite) TestCreate_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "inbox_id": s.testInbox.ID, "contact_id": s.testContact.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/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 *ConversationCrudTestSuite) TestCreate_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestCreate_MissingRequiredFields() { body, _ := json.Marshal(map[string]interface{}{ "inbox_id": s.testInbox.ID, // missing contact_id }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Service validation should return an error (validation/required) assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusUnprocessableEntity) } // ========== Get Handler Tests ========== func (s *ConversationCrudTestSuite) TestGet_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), float64(s.testConv.ID), resp["id"]) assert.NotNil(s.T(), resp["messages"]) } func (s *ConversationCrudTestSuite) TestGet_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestGet_InvalidConversationID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/invalid", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestGet_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.convURL(99999), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsBoundAssistant() { assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Inbox Helper", Description: "Helps this inbox", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive} s.Require().NoError(s.db.Create(assistant).Error) s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil) s.router.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)) assistantResp, ok := resp["assistant"].(map[string]interface{}) s.Require().True(ok) assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"]) assert.Equal(s.T(), "Inbox Helper", assistantResp["name"]) assert.NotContains(s.T(), assistantResp, "description") } func (s *ConversationCrudTestSuite) TestInboxAssistant_ReturnsNilWhenUnbound() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.convURL(s.testConv.ID)+"/inbox_assistant", nil) s.router.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.Contains(s.T(), resp, "assistant") assert.Nil(s.T(), resp["assistant"]) } func (s *ConversationCrudTestSuite) TestInboxAssistant_UsesDisplayIDRoute() { displayID := uint(77) s.testConv.DisplayID = &displayID s.Require().NoError(s.db.Save(s.testConv).Error) assistant := &model.CaptainAssistant{AccountID: s.testAccount.ID, Name: "Display Helper", Description: "Display route", Config: json.RawMessage(`{}`), Status: model.AssistantStatusActive} s.Require().NoError(s.db.Create(assistant).Error) s.Require().NoError(s.db.Create(&model.CaptainInbox{AccountID: s.testAccount.ID, AssistantID: assistant.ID, InboxID: s.testInbox.ID}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/77/inbox_assistant", nil) s.router.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)) assistantResp := resp["assistant"].(map[string]interface{}) assert.Equal(s.T(), float64(assistant.ID), assistantResp["id"]) } // ========== Update Handler Tests ========== func (s *ConversationCrudTestSuite) TestUpdate_Success() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", "priority": "high", }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.convURL(s.testConv.ID), 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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), "resolved", resp["status"]) } func (s *ConversationCrudTestSuite) TestUpdate_WithSlaPolicyCreatesAppliedSlaPayload() { policy := &model.SlaPolicy{ AccountID: s.testAccount.ID, Name: "Gold SLA", Description: "Priority customers", FirstResponseTimeThreshold: 15, NextResponseTimeThreshold: 30, ResolutionTimeThreshold: 120, } s.Require().NoError(s.db.Create(policy).Error) body, _ := json.Marshal(map[string]interface{}{ "sla_policy_id": policy.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.convURL(s.testConv.ID), 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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), float64(policy.ID), resp["sla_policy_id"]) applied, ok := resp["applied_sla"].(map[string]interface{}) s.Require().True(ok) assert.Equal(s.T(), float64(policy.ID), applied["sla_id"]) assert.Equal(s.T(), "active", applied["sla_status"]) assert.Equal(s.T(), "Gold SLA", applied["sla_name"]) assert.Equal(s.T(), float64(15), applied["sla_first_response_time_threshold"]) var count int64 s.Require().NoError(s.db.Model(&model.AppliedSLA{}).Where("conversation_id = ?", s.testConv.ID).Count(&count).Error) assert.Equal(s.T(), int64(1), count) } func (s *ConversationCrudTestSuite) TestUpdate_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10), 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 *ConversationCrudTestSuite) TestUpdate_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.accountURL()+"/conversations/invalid", 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 *ConversationCrudTestSuite) TestUpdate_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.convURL(s.testConv.ID), bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestUpdate_NotFound() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.convURL(99999), bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== Delete Handler Tests ========== func (s *ConversationCrudTestSuite) TestDelete_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.convURL(s.testConv.ID), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNoContent, w.Code) } func (s *ConversationCrudTestSuite) TestDelete_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestDelete_InvalidConversationID() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.accountURL()+"/conversations/invalid", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestDelete_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.convURL(99999), nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== AssignAgent Handler Tests ========== func (s *ConversationCrudTestSuite) TestAssignAgent_Success() { // Create a user to assign user := &model.User{Name: "AgentUser", Email: "agent@test.com"} s.Require().NoError(s.db.Create(user).Error) // Add as inbox member inboxMember := &model.InboxMember{InboxID: s.testInbox.ID, UserID: user.ID} s.Require().NoError(s.db.Create(inboxMember).Error) // Create account_user to link user to account accountUser := &model.AccountUser{AccountID: s.testAccount.ID, UserID: user.ID, Role: "agent"} s.Require().NoError(s.db.Create(accountUser).Error) body, _ := json.Marshal(map[string]interface{}{ "assignee_id": user.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), float64(user.ID), resp["id"]) } func (s *ConversationCrudTestSuite) TestAssignAgent_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "assignee_id": 1, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/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 *ConversationCrudTestSuite) TestAssignAgent_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "assignee_id": 1, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/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 *ConversationCrudTestSuite) TestAssignAgent_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/assign", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ========== ToggleStatus Handler Tests ========== func (s *ConversationCrudTestSuite) TestToggleStatus_Success() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/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 struct { Payload struct { CurrentStatus string `json:"current_status"` } `json:"payload"` } err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), "resolved", resp.Payload.CurrentStatus) } func (s *ConversationCrudTestSuite) TestToggleStatus_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/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 *ConversationCrudTestSuite) TestToggleStatus_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/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 *ConversationCrudTestSuite) TestToggleStatus_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestToggleStatus_InvalidStatusValue() { body, _ := json.Marshal(map[string]interface{}{ "status": "invalid_status", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Should be 400 (validation error) or 500 (service error) assert.True(s.T(), w.Code == http.StatusBadRequest || w.Code == http.StatusUnprocessableEntity) } func (s *ConversationCrudTestSuite) TestToggleStatus_NotFound() { body, _ := json.Marshal(map[string]interface{}{ "status": "resolved", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/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) } // ========== Mute Handler Tests ========== func (s *ConversationCrudTestSuite) TestMute_Success() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/mute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) assert.Empty(s.T(), w.Body.String()) } func (s *ConversationCrudTestSuite) TestMute_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/mute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestMute_InvalidConversationID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/mute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestMute_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/mute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== Unmute Handler Tests ========== func (s *ConversationCrudTestSuite) TestUnmute_Success() { // First mute the conversation s.handler.conversationSvc.Mute(context.Background(), s.testAccount.ID, s.testConv.ID) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/unmute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) assert.Empty(s.T(), w.Body.String()) } func (s *ConversationCrudTestSuite) TestUnmute_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/unmute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestUnmute_InvalidConversationID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/unmute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestUnmute_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/unmute", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== UpdateLabels Handler Tests ========== func (s *ConversationCrudTestSuite) TestUpdateLabels_Success() { body, _ := json.Marshal(map[string]interface{}{ "labels": []string{"support", "bug"}, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.ElementsMatch(s.T(), []interface{}{"support", "bug"}, resp["labels"]) } func (s *ConversationCrudTestSuite) TestUpdateLabels_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "labels": []string{"support"}, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/labels", 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 *ConversationCrudTestSuite) TestUpdateLabels_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "labels": []string{"support"}, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/labels", 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 *ConversationCrudTestSuite) TestUpdateLabels_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/labels", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestUpdateLabels_NotFound() { body, _ := json.Marshal(map[string]interface{}{ "labels": []string{"support"}, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/labels", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== Search Handler Tests ========== func (s *ConversationCrudTestSuite) TestSearch_Success() { s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, SenderID: &s.testContact.ID, SenderType: string(model.SenderTypeContact), Content: "needle open", MessageType: string(model.MessageTypeIncoming)}).Error) agent := &model.User{AccountID: s.testAccount.ID, Name: "Search Agent", Email: "search-agent-" + strconv.FormatUint(uint64(s.testAccount.ID), 10) + "@example.com", Password: "secret", Role: "agent"} s.Require().NoError(s.db.Create(agent).Error) resolved := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "resolved", ChannelType: "web_widget", Channel: "web_widget"} s.Require().NoError(s.db.Create(resolved).Error) s.Require().NoError(s.db.Create(&model.Message{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: resolved.ID, SenderID: &agent.ID, SenderType: string(model.SenderTypeUser), Content: "needle resolved", MessageType: string(model.MessageTypeOutgoing)}).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search?q=needle&status=open", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Meta struct { MineCount int64 `json:"mine_count"` UnassignedCount int64 `json:"unassigned_count"` AllCount int64 `json:"all_count"` } `json:"meta"` Payload []struct { ID uint `json:"id"` Contact map[string]any `json:"contact"` Inbox map[string]any `json:"inbox"` Messages []map[string]any `json:"messages"` } `json:"payload"` } err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.NotContains(s.T(), w.Body.String(), "data") var raw map[string]any s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &raw)) rawMeta := raw["meta"].(map[string]any) assert.NotContains(s.T(), rawMeta, "assigned_count") rawPayload := raw["payload"].([]any) s.Require().NotEmpty(rawPayload) rawConversation := rawPayload[0].(map[string]any) assert.NotContains(s.T(), rawConversation, "meta") assert.Equal(s.T(), int64(2), resp.Meta.AllCount) assert.Equal(s.T(), int64(2), resp.Meta.UnassignedCount) assert.Len(s.T(), resp.Payload, 2) assert.NotZero(s.T(), resp.Payload[0].ID) assert.Equal(s.T(), s.testContact.Name, resp.Payload[0].Contact["name"]) assert.Equal(s.T(), s.testInbox.Name, resp.Payload[0].Inbox["name"]) assert.NotEmpty(s.T(), resp.Payload[0].Messages) assert.Equal(s.T(), float64(0), resp.Payload[0].Messages[0]["message_type"]) senderNamesByContent := map[string]string{} for _, conversation := range resp.Payload { for _, message := range conversation.Messages { if senderName, ok := message["sender_name"].(string); ok { senderNamesByContent[message["content"].(string)] = senderName } } } assert.Equal(s.T(), s.testContact.Name, senderNamesByContent["needle open"]) assert.Equal(s.T(), agent.Name, senderNamesByContent["needle resolved"]) } func (s *ConversationCrudTestSuite) TestSearch_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/invalid/conversations/search?q=test", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestSearch_MissingQuery() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/conversations/search", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]any `json:"payload"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(s.T(), int64(1), resp.Meta.AllCount) assert.Len(s.T(), resp.Payload, 1) } // ========== Filter Handler Tests ========== func (s *ConversationCrudTestSuite) TestFilter_Success() { body, _ := json.Marshal(map[string]interface{}{ "status": "open", "assignee_type": "all", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/filter", 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 struct { Meta struct { AllCount int64 `json:"all_count"` } `json:"meta"` Payload []map[string]interface{} `json:"payload"` } err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), int64(1), resp.Meta.AllCount) assert.Len(s.T(), resp.Payload, 1) } func (s *ConversationCrudTestSuite) TestFilter_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "status": "open", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/filter", 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 *ConversationCrudTestSuite) TestFilter_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/filter", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } // ========== UpdatePriority Handler Tests ========== func (s *ConversationCrudTestSuite) TestUpdatePriority_Success() { body, _ := json.Marshal(map[string]interface{}{ "priority": "urgent", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/priority", 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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), "urgent", resp["priority"]) } func (s *ConversationCrudTestSuite) TestUpdatePriority_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "priority": "urgent", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/priority", 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 *ConversationCrudTestSuite) TestUpdatePriority_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "priority": "urgent", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/priority", 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 *ConversationCrudTestSuite) TestUpdatePriority_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/priority", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestUpdatePriority_InvalidPriorityValue() { body, _ := json.Marshal(map[string]interface{}{ "priority": "invalid_priority", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/priority", 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 *ConversationCrudTestSuite) TestUpdatePriority_NotFound() { body, _ := json.Marshal(map[string]interface{}{ "priority": "urgent", }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/priority", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== AssignTeam Handler Tests ========== func (s *ConversationCrudTestSuite) TestAssignTeam_Success() { // Create a team team := &model.Team{AccountID: s.testAccount.ID, Name: "TestTeam"} s.Require().NoError(s.db.Create(team).Error) body, _ := json.Marshal(map[string]interface{}{ "team_id": team.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/assignments", 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{} err := json.Unmarshal(w.Body.Bytes(), &resp) assert.NoError(s.T(), err) assert.Equal(s.T(), float64(team.ID), resp["id"]) assert.Equal(s.T(), "TestTeam", resp["name"]) assert.Equal(s.T(), float64(s.testAccount.ID), resp["account_id"]) assert.Nil(s.T(), resp["meta"]) } func (s *ConversationCrudTestSuite) TestAssignTeam_WithAgentID() { // Create a user and team user := &model.User{Name: "TeamAgentUser", Email: "teamagent@test.com"} s.Require().NoError(s.db.Create(user).Error) accountUser := &model.AccountUser{AccountID: s.testAccount.ID, UserID: user.ID, Role: "agent"} s.Require().NoError(s.db.Create(accountUser).Error) inboxMember := &model.InboxMember{InboxID: s.testInbox.ID, UserID: user.ID} s.Require().NoError(s.db.Create(inboxMember).Error) team := &model.Team{AccountID: s.testAccount.ID, Name: "TestTeam2"} s.Require().NoError(s.db.Create(team).Error) body, _ := json.Marshal(map[string]interface{}{ "agent_id": user.ID, "team_id": team.ID, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/assignments", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *ConversationCrudTestSuite) TestAssignTeam_InvalidAccountID() { body, _ := json.Marshal(map[string]interface{}{ "team_id": 1, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/invalid/conversations/"+strconv.FormatUint(uint64(s.testConv.ID), 10)+"/assignments", 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 *ConversationCrudTestSuite) TestAssignTeam_InvalidConversationID() { body, _ := json.Marshal(map[string]interface{}{ "team_id": 1, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/conversations/invalid/assignments", 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 *ConversationCrudTestSuite) TestAssignTeam_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/assignments", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *ConversationCrudTestSuite) TestAssignTeam_NotFound() { body, _ := json.Marshal(map[string]interface{}{ "team_id": 1, }) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.convURL(99999)+"/assignments", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } // ========== Test runner ========== func TestConversationCrudTestSuite(t *testing.T) { suite.Run(t, new(ConversationCrudTestSuite)) }