package e2e import ( "bytes" "encoding/json" "fmt" "net/http" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "github.com/gochat/gochat/internal/model" ) // ConversationE2ETestSuite tests the full conversation flow end-to-end: // Create Inbox → Start Conversation → Send Messages → Resolve // Reference: Chatwoot spec/controllers/api/v1/accounts/conversations_controller_spec.rb type ConversationE2ETestSuite struct { E2ETestSuite authToken string account *model.Account inbox *model.Inbox contact *model.Contact } // SetupTest creates fresh data for each test. func (s *ConversationE2ETestSuite) SetupTest() { s.ClearDatabase() s.account = s.CreateTestAccount("Conv Test Org") s.inbox = s.CreateTestInbox("Support Inbox", "web_widget", s.account.ID) s.contact = s.CreateTestContact("Conv Contact", "conv@test.com", s.account.ID) // In a real E2E test, we'd authenticate and get a real token // For now we'll test the DB-level operations as part of the flow } // TestCreateInbox verifies inbox creation via API or DB. func (s *ConversationE2ETestSuite) TestCreateInbox() { assert.NotNil(s.T(), s.inbox) assert.Equal(s.T(), "Support Inbox", s.inbox.Name) assert.Equal(s.T(), "web_widget", s.inbox.ChannelType) assert.Equal(s.T(), s.account.ID, s.inbox.AccountID) // Verify in database var retrieved model.Inbox err := s.DB().First(&retrieved, s.inbox.ID).Error assert.NoError(s.T(), err) assert.Equal(s.T(), s.inbox.Name, retrieved.Name) } // TestCreateMultipleInboxTypes verifies creating inboxes for different channel types. func (s *ConversationE2ETestSuite) TestCreateMultipleInboxTypes() { inboxes := []struct { name string channel string }{ {"Web Widget Inbox", "web_widget"}, {"Telegram Inbox", "telegram"}, {"Facebook Inbox", "facebook"}, {"WhatsApp Inbox", "whatsapp"}, {"Email Inbox", "email"}, } for _, tc := range inboxes { inbox := s.CreateTestInbox(tc.name, tc.channel, s.account.ID) assert.NotNil(s.T(), inbox) assert.Equal(s.T(), tc.channel, inbox.ChannelType) } } // TestStartConversation verifies conversation creation. func (s *ConversationE2ETestSuite) TestStartConversation() { conv := s.CreateTestConversation( s.account.ID, s.inbox.ID, s.contact.ID, "open", ) assert.NotNil(s.T(), conv) assert.Equal(s.T(), "open", conv.Status) assert.Equal(s.T(), s.account.ID, conv.AccountID) assert.Equal(s.T(), s.inbox.ID, conv.InboxID) assert.Equal(s.T(), s.contact.ID, conv.ContactID) assert.Equal(s.T(), "web_widget", conv.ChannelType) // Verify conversation exists in database var retrieved model.Conversation err := s.DB().First(&retrieved, conv.ID).Error assert.NoError(s.T(), err) assert.Equal(s.T(), "open", retrieved.Status) } // TestSendMessageToConversation verifies message creation within a conversation. func (s *ConversationE2ETestSuite) TestSendMessageToConversation() { conv := s.CreateTestConversation( s.account.ID, s.inbox.ID, s.contact.ID, "open", ) // Contact sends message msg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: s.inbox.ID, SenderID: &s.contact.ID, SenderType: "contact", Content: "Hello, I need help with my order", ContentType: "text", MessageType: "incoming", } err := s.DB().Create(msg).Error assert.NoError(s.T(), err) assert.NotZero(s.T(), msg.ID) // Agent replies agentUser := s.CreateTestUser("agent@conv.com", "Agent Smith", "hashedpass", "agent", s.account.ID) reply := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: s.inbox.ID, SenderID: &agentUser.ID, SenderType: "agent", Content: "Hi! I'm happy to help with your order. Could you provide the order number?", ContentType: "text", MessageType: "outgoing", } err = s.DB().Create(reply).Error assert.NoError(s.T(), err) assert.NotZero(s.T(), reply.ID) } // TestConversationStatusTransitions verifies status change lifecycle. func (s *ConversationE2ETestSuite) TestConversationStatusTransitions() { conv := s.CreateTestConversation( s.account.ID, s.inbox.ID, s.contact.ID, "open", ) // Open → Pending conv.Status = "pending" err := s.DB().Save(conv).Error assert.NoError(s.T(), err) var retrieved model.Conversation s.DB().First(&retrieved, conv.ID) assert.Equal(s.T(), "pending", retrieved.Status) // Pending → Open (reopened) conv.Status = "open" err = s.DB().Save(conv).Error assert.NoError(s.T(), err) s.DB().First(&retrieved, conv.ID) assert.Equal(s.T(), "open", retrieved.Status) // Open → Resolved conv.Status = "resolved" err = s.DB().Save(conv).Error assert.NoError(s.T(), err) s.DB().First(&retrieved, conv.ID) assert.Equal(s.T(), "resolved", retrieved.Status) } // TestAssignConversationToAgent verifies agent assignment. func (s *ConversationE2ETestSuite) TestAssignConversationToAgent() { conv := s.CreateTestConversation( s.account.ID, s.inbox.ID, s.contact.ID, "open", ) agentUser := s.CreateTestUser("assign@conv.com", "Assigned Agent", "hashedpass", "agent", s.account.ID) // Assign conversation to agent conv.AssigneeID = &agentUser.ID err := s.DB().Save(conv).Error assert.NoError(s.T(), err) var retrieved model.Conversation s.DB().First(&retrieved, conv.ID) assert.NotNil(s.T(), retrieved.AssigneeID) assert.Equal(s.T(), agentUser.ID, *retrieved.AssigneeID) } // TestUnassignConversation verifies removing assignment. func (s *ConversationE2ETestSuite) TestUnassignConversation() { agentUser := s.CreateTestUser("unassign@conv.com", "Unassigned Agent", "hashedpass", "agent", s.account.ID) conv := s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") conv.AssigneeID = &agentUser.ID s.DB().Save(conv) // Unassign conv.AssigneeID = nil err := s.DB().Save(conv).Error assert.NoError(s.T(), err) var retrieved model.Conversation s.DB().First(&retrieved, conv.ID) assert.Nil(s.T(), retrieved.AssigneeID) } // TestListConversationsByAccount verifies listing conversations. func (s *ConversationE2ETestSuite) TestListConversationsByAccount() { // Create multiple conversations for i := 0; i < 5; i++ { s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") } var conversations []model.Conversation err := s.DB().Where("account_id = ?", s.account.ID).Find(&conversations).Error assert.NoError(s.T(), err) assert.Equal(s.T(), 5, len(conversations)) } // TestFilterConversationsByStatus verifies status filtering. func (s *ConversationE2ETestSuite) TestFilterConversationsByStatus() { // Create conversations with different statuses s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "pending") s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "resolved") var openConvs []model.Conversation err := s.DB().Where("account_id = ? AND status = ?", s.account.ID, "open").Find(&openConvs).Error assert.NoError(s.T(), err) assert.Equal(s.T(), 2, len(openConvs)) var resolvedConvs []model.Conversation err = s.DB().Where("account_id = ? AND status = ?", s.account.ID, "resolved").Find(&resolvedConvs).Error assert.NoError(s.T(), err) assert.Equal(s.T(), 1, len(resolvedConvs)) } // TestMultipleMessagesInConversation verifies message ordering and count. func (s *ConversationE2ETestSuite) TestMultipleMessagesInConversation() { conv := s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") for i := 0; i < 10; i++ { msg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: s.inbox.ID, SenderID: &s.contact.ID, SenderType: "contact", Content: fmt.Sprintf("Message %d", i), ContentType: "text", MessageType: "incoming", } err := s.DB().Create(msg).Error assert.NoError(s.T(), err) } var messages []model.Message err := s.DB().Where("conversation_id = ?", conv.ID).Order("id ASC").Find(&messages).Error assert.NoError(s.T(), err) assert.Equal(s.T(), 10, len(messages)) // Verify ordering for i, msg := range messages { assert.Equal(s.T(), fmt.Sprintf("Message %d", i), msg.Content) } } // TestPrivateMessages verifies private (internal) notes. func (s *ConversationE2ETestSuite) TestPrivateMessages() { conv := s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") agentUser := s.CreateTestUser("private@conv.com", "Private Agent", "hashedpass", "agent", s.account.ID) // Private note privateMsg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: s.inbox.ID, SenderID: &agentUser.ID, SenderType: "agent", Content: "Internal note: customer seems frustrated", ContentType: "text", MessageType: "outgoing", Private: true, } err := s.DB().Create(privateMsg).Error assert.NoError(s.T(), err) assert.True(s.T(), privateMsg.Private) // Public message publicMsg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: s.inbox.ID, SenderID: &agentUser.ID, SenderType: "agent", Content: "We're looking into this for you", ContentType: "text", MessageType: "outgoing", Private: false, } err = s.DB().Create(publicMsg).Error assert.NoError(s.T(), err) assert.False(s.T(), publicMsg.Private) // Count private vs public var privateCount int64 s.DB().Model(&model.Message{}).Where("conversation_id = ? AND private = ?", conv.ID, true).Count(&privateCount) assert.Equal(s.T(), int64(1), privateCount) var publicCount int64 s.DB().Model(&model.Message{}).Where("conversation_id = ? AND private = ?", conv.ID, false).Count(&publicCount) assert.Equal(s.T(), int64(1), publicCount) } // TestConversationAPIEndpoint tests the HTTP API for conversations. func (s *ConversationE2ETestSuite) TestConversationAPIEndpoint() { // Create conversations via HTTP (if endpoint available) payload := map[string]interface{}{ "account_id": s.account.ID, "inbox_id": s.inbox.ID, "contact_id": s.contact.ID, "status": "open", } body, _ := json.Marshal(payload) resp, err := http.Post( fmt.Sprintf("%s/api/v1/accounts/%d/conversations", s.ServerURL(), s.account.ID), "application/json", bytes.NewBuffer(body), ) if err != nil { s.T().Logf("Conversation API endpoint not available yet: %v", err) return } defer resp.Body.Close() if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) assert.NotNil(s.T(), result["id"], "Created conversation should have an ID") } } // TestSendMessageAPIEndpoint tests sending a message via HTTP. func (s *ConversationE2ETestSuite) TestSendMessageAPIEndpoint() { conv := s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") payload := map[string]interface{}{ "content": "API test message", "content_type": "text", "message_type": "outgoing", "private": false, } body, _ := json.Marshal(payload) resp, err := http.Post( fmt.Sprintf("%s/api/v1/accounts/%d/conversations/%d/messages", s.ServerURL(), s.account.ID, conv.ID), "application/json", bytes.NewBuffer(body), ) if err != nil { s.T().Logf("Message API endpoint not available yet: %v", err) return } defer resp.Body.Close() if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) assert.NotNil(s.T(), result["id"], "Created message should have an ID") } } // TestConversationAcrossMultipleInboxes verifies conversations in different inboxes. func (s *ConversationE2ETestSuite) TestConversationAcrossMultipleInboxes() { inbox1 := s.CreateTestInbox("Inbox 1", "web_widget", s.account.ID) inbox2 := s.CreateTestInbox("Inbox 2", "telegram", s.account.ID) conv1 := s.CreateTestConversation(s.account.ID, inbox1.ID, s.contact.ID, "open") conv2 := s.CreateTestConversation(s.account.ID, inbox2.ID, s.contact.ID, "open") assert.Equal(s.T(), inbox1.ID, conv1.InboxID) assert.Equal(s.T(), inbox2.ID, conv2.InboxID) // Each conversation should have different channel type assert.Equal(s.T(), "web_widget", conv1.ChannelType) assert.Equal(s.T(), "telegram", conv2.ChannelType) } // TestContactInboxBinding verifies contact-to-inbox binding. func (s *ConversationE2ETestSuite) TestContactInboxBinding() { contactInbox := &model.ContactInbox{ ContactID: s.contact.ID, InboxID: s.inbox.ID, SourceID: "widget_source_123", } err := s.DB().Create(contactInbox).Error assert.NoError(s.T(), err) assert.NotZero(s.T(), contactInbox.ID) // Verify binding var retrieved model.ContactInbox err = s.DB().Where("contact_id = ? AND inbox_id = ?", s.contact.ID, s.inbox.ID).First(&retrieved).Error assert.NoError(s.T(), err) assert.Equal(s.T(), "widget_source_123", retrieved.SourceID) } // TestConversationLastMessageAt tracks last message timing. func (s *ConversationE2ETestSuite) TestConversationLastMessageAt() { conv := s.CreateTestConversation(s.account.ID, s.inbox.ID, s.contact.ID, "open") now := time.Now().Unix() conv.LastMessageAt = &now err := s.DB().Save(conv).Error assert.NoError(s.T(), err) var retrieved model.Conversation s.DB().First(&retrieved, conv.ID) assert.NotNil(s.T(), retrieved.LastMessageAt) } // TestFullConversationLifecycle is the comprehensive E2E flow. func (s *ConversationE2ETestSuite) TestFullConversationLifecycle() { // 1. Create inbox inbox := s.CreateTestInbox("Lifecycle Inbox", "web_widget", s.account.ID) // 2. Create contact contact := s.CreateTestContact("Lifecycle Contact", "lc@test.com", s.account.ID) // 3. Bind contact to inbox contactInbox := &model.ContactInbox{ ContactID: contact.ID, InboxID: inbox.ID, SourceID: "lifecycle_source", } err := s.DB().Create(contactInbox).Error assert.NoError(s.T(), err) // 4. Create conversation conv := s.CreateTestConversation(s.account.ID, inbox.ID, contact.ID, "open") // 5. Contact sends message contactMsg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: inbox.ID, SenderID: &contact.ID, SenderType: "contact", Content: "I need help!", ContentType: "text", MessageType: "incoming", } err = s.DB().Create(contactMsg).Error assert.NoError(s.T(), err) // 6. Assign to agent agent := s.CreateTestUser("lcagent@test.com", "Lifecycle Agent", "hashedpass", "agent", s.account.ID) conv.AssigneeID = &agent.ID err = s.DB().Save(conv).Error assert.NoError(s.T(), err) // 7. Agent responds agentMsg := &model.Message{ ConversationID: conv.ID, AccountID: s.account.ID, InboxID: inbox.ID, SenderID: &agent.ID, SenderType: "agent", Content: "Sure, I can help you!", ContentType: "text", MessageType: "outgoing", } err = s.DB().Create(agentMsg).Error assert.NoError(s.T(), err) // 8. Resolve conversation conv.Status = "resolved" err = s.DB().Save(conv).Error assert.NoError(s.T(), err) // Verify final state var final model.Conversation s.DB().First(&final, conv.ID) assert.Equal(s.T(), "resolved", final.Status) assert.NotNil(s.T(), final.AssigneeID) assert.Equal(s.T(), agent.ID, *final.AssigneeID) // Verify messages var msgs []model.Message s.DB().Where("conversation_id = ?", conv.ID).Order("id ASC").Find(&msgs) assert.Equal(s.T(), 2, len(msgs)) assert.Equal(s.T(), "I need help!", msgs[0].Content) assert.Equal(s.T(), "Sure, I can help you!", msgs[1].Content) } // TestConversationE2ESuite runs the Conversation E2E test suite. func TestConversationE2ESuite(t *testing.T) { suite.Run(t, new(ConversationE2ETestSuite)) }