1710 lines
64 KiB
Go
1710 lines
64 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/datatypes"
|
|
"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{},
|
|
&model.CustomAttributeDefinition{},
|
|
)
|
|
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/toggle_priority", handler.TogglePriority)
|
|
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")
|
|
s.db.Exec("DELETE FROM custom_attribute_definitions")
|
|
}
|
|
|
|
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_NoStatusTogglesLikeChatwoot() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp struct {
|
|
Payload struct {
|
|
Success bool `json:"success"`
|
|
CurrentStatus string `json:"current_status"`
|
|
} `json:"payload"`
|
|
}
|
|
err := json.Unmarshal(w.Body.Bytes(), &resp)
|
|
assert.NoError(s.T(), err)
|
|
assert.True(s.T(), resp.Payload.Success)
|
|
assert.Equal(s.T(), "resolved", resp.Payload.CurrentStatus)
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestToggleStatus_NoStatusReopensPendingLikeChatwoot() {
|
|
s.Require().NoError(s.db.Model(&model.Conversation{}).Where("id = ?", s.testConv.ID).Update("status", "pending").Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_status", bytes.NewReader([]byte(`{}`)))
|
|
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(), "open", 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())
|
|
|
|
var conversation model.Conversation
|
|
s.Require().NoError(s.db.First(&conversation, s.testConv.ID).Error)
|
|
assert.Equal(s.T(), string(model.ConversationStatusResolved), conversation.Status)
|
|
assert.True(s.T(), conversation.Muted)
|
|
|
|
var contact model.Contact
|
|
s.Require().NoError(s.db.First(&contact, s.testContact.ID).Error)
|
|
assert.True(s.T(), contact.Blocked)
|
|
|
|
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{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), true, resp["muted"])
|
|
assert.Equal(s.T(), string(model.ConversationStatusResolved), resp["status"])
|
|
}
|
|
|
|
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())
|
|
|
|
var conversation model.Conversation
|
|
s.Require().NoError(s.db.First(&conversation, s.testConv.ID).Error)
|
|
assert.Equal(s.T(), string(model.ConversationStatusResolved), conversation.Status)
|
|
assert.False(s.T(), conversation.Muted)
|
|
|
|
var contact model.Contact
|
|
s.Require().NoError(s.db.First(&contact, s.testContact.ID).Error)
|
|
assert.False(s.T(), contact.Blocked)
|
|
}
|
|
|
|
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() {
|
|
newer := time.Date(2026, 6, 7, 12, 0, 0, 0, time.UTC)
|
|
older := newer.Add(-time.Hour)
|
|
s.Require().NoError(s.db.Create(&model.Message{Base: model.Base{CreatedAt: newer}, 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)
|
|
s.Require().NoError(s.db.Create(&model.Message{Base: model.Base{CreatedAt: older}, AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ConversationID: s.testConv.ID, SenderID: &s.testContact.ID, SenderType: string(model.SenderTypeContact), Content: "older context", 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)
|
|
senderNamesByContent := map[string]string{}
|
|
messageTypesByContent := map[string]float64{}
|
|
contentsByConversation := map[uint][]string{}
|
|
for _, conversation := range resp.Payload {
|
|
for _, message := range conversation.Messages {
|
|
content := message["content"].(string)
|
|
contentsByConversation[conversation.ID] = append(contentsByConversation[conversation.ID], content)
|
|
messageTypesByContent[content] = message["message_type"].(float64)
|
|
if senderName, ok := message["sender_name"].(string); ok {
|
|
senderNamesByContent[content] = senderName
|
|
}
|
|
}
|
|
}
|
|
assert.Equal(s.T(), []string{"older context", "needle open"}, contentsByConversation[s.testConv.ID])
|
|
assert.Equal(s.T(), float64(0), messageTypesByContent["needle open"])
|
|
assert.Equal(s.T(), float64(1), messageTypesByContent["needle resolved"])
|
|
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_ChatwootPayloadStatus() {
|
|
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)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "status",
|
|
"filter_operator": "equal_to",
|
|
"values": []string{"open"},
|
|
},
|
|
},
|
|
})
|
|
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)
|
|
assert.NotContains(s.T(), w.Body.String(), "data")
|
|
|
|
var resp struct {
|
|
Meta struct {
|
|
AllCount int64 `json:"all_count"`
|
|
UnassignedCount int64 `json:"unassigned_count"`
|
|
} `json:"meta"`
|
|
Payload []map[string]interface{} `json:"payload"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
assert.Equal(s.T(), int64(1), resp.Meta.UnassignedCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadQueryOperatorChain() {
|
|
primaryID := uint(12345)
|
|
s.testConv.Status = "open"
|
|
s.testConv.Priority = "urgent"
|
|
s.testConv.DisplayID = &primaryID
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
secondaryID := uint(67890)
|
|
secondary := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &secondaryID, Status: "resolved", Priority: "low", ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(secondary).Error)
|
|
|
|
nonMatchingID := uint(11111)
|
|
nonMatching := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &nonMatchingID, Status: "resolved", Priority: "low", ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(nonMatching).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{"attribute_key": "status", "filter_operator": "equal_to", "values": []string{"open"}, "query_operator": "OR"},
|
|
{"attribute_key": "priority", "filter_operator": "equal_to", "values": []string{"low"}, "query_operator": "AND"},
|
|
{"attribute_key": "display_id", "filter_operator": "equal_to", "values": []string{"67890"}},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(2), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 2)
|
|
ids := []float64{resp.Payload[0]["id"].(float64), resp.Payload[1]["id"].(float64)}
|
|
assert.ElementsMatch(s.T(), []float64{float64(primaryID), float64(secondaryID)}, ids)
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadAdditionalAttribute() {
|
|
s.testConv.AdditionalAttributes = datatypes.JSON(`{"browser_language":"en"}`)
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
secondary := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "open", AdditionalAttributes: datatypes.JSON(`{"browser_language":"fr"}`), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(secondary).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "browser_language",
|
|
"filter_operator": "equal_to",
|
|
"values": []string{"en"},
|
|
},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadAdditionalAttributeContains() {
|
|
s.testConv.AdditionalAttributes = datatypes.JSON(`{"mail_subject":"Welcome to Billing"}`)
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
secondary := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "open", AdditionalAttributes: datatypes.JSON(`{"mail_subject":"Shipping update"}`), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(secondary).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "mail_subject",
|
|
"filter_operator": "contains",
|
|
"values": []string{"billing"},
|
|
},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadCustomAttributeNotEqualIncludesNull() {
|
|
def := &model.CustomAttributeDefinition{
|
|
AccountID: s.testAccount.ID,
|
|
AttributeName: "conversation_type",
|
|
AttributeDisplayName: "Conversation type",
|
|
AttributeType: "list",
|
|
AttributeModel: "conversation_attribute",
|
|
AttributeValues: datatypes.JSON(`["platinum","silver"]`),
|
|
}
|
|
s.Require().NoError(s.db.Create(def).Error)
|
|
|
|
platinumID := uint(22201)
|
|
s.testConv.DisplayID = &platinumID
|
|
s.testConv.CustomAttributes = datatypes.JSON(`{"conversation_type":"platinum"}`)
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
silverID := uint(22202)
|
|
silver := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &silverID, Status: "open", CustomAttributes: datatypes.JSON(`{"conversation_type":"silver"}`), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(silver).Error)
|
|
|
|
nilID := uint(22203)
|
|
missing := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &nilID, Status: "open", CustomAttributes: datatypes.JSON(`{}`), ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(missing).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "conversation_type",
|
|
"custom_attribute_type": "conversation_attribute",
|
|
"filter_operator": "not_equal_to",
|
|
"values": []string{"platinum"},
|
|
},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(2), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 2)
|
|
ids := []float64{resp.Payload[0]["id"].(float64), resp.Payload[1]["id"].(float64)}
|
|
assert.ElementsMatch(s.T(), []float64{float64(silverID), float64(nilID)}, ids)
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadStandardPresenceFilters() {
|
|
agentID := uint(7001)
|
|
teamID := uint(7002)
|
|
campaignID := uint(7003)
|
|
s.testConv.AssigneeID = &agentID
|
|
s.testConv.TeamID = &teamID
|
|
s.testConv.CampaignID = &campaignID
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
unassigned := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(unassigned).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{"attribute_key": "assignee_id", "filter_operator": "is_present", "values": []string{}, "query_operator": "AND"},
|
|
{"attribute_key": "team_id", "filter_operator": "equal_to", "values": []string{"7002"}, "query_operator": "AND"},
|
|
{"attribute_key": "campaign_id", "filter_operator": "is_present", "values": []string{}},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadLabels() {
|
|
vip := &model.Tag{AccountID: s.testAccount.ID, Name: "vip", Color: "#1f93ff"}
|
|
s.Require().NoError(s.db.Create(vip).Error)
|
|
s.Require().NoError(s.db.Create(&model.ConversationLabel{AccountID: s.testAccount.ID, ConversationID: s.testConv.ID, TagID: vip.ID}).Error)
|
|
|
|
plainID := uint(33303)
|
|
plain := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &plainID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(plain).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{"attribute_key": "labels", "filter_operator": "equal_to", "values": []string{"vip"}},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadDisplayIDContainsAndDate() {
|
|
primaryID := uint(45678)
|
|
s.testConv.DisplayID = &primaryID
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
s.Require().NoError(s.db.Model(&model.Conversation{}).Where("id = ?", s.testConv.ID).Update("created_at", time.Date(2024, 1, 20, 10, 0, 0, 0, time.UTC)).Error)
|
|
|
|
olderID := uint(12345)
|
|
older := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, DisplayID: &olderID, Status: "open", ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(older).Error)
|
|
s.Require().NoError(s.db.Model(&model.Conversation{}).Where("id = ?", older.ID).Update("created_at", time.Date(2024, 1, 1, 10, 0, 0, 0, time.UTC)).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{"attribute_key": "display_id", "filter_operator": "contains", "values": []string{"567"}, "query_operator": "AND"},
|
|
{"attribute_key": "created_at", "filter_operator": "is_greater_than", "values": []string{"2024-01-10"}},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(primaryID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadLastActivityAtDaysBefore() {
|
|
recentActivity := time.Now().UTC().AddDate(0, 0, -1).Unix()
|
|
oldActivity := time.Now().UTC().AddDate(0, 0, -12).Unix()
|
|
s.testConv.LastActivityAt = &oldActivity
|
|
s.Require().NoError(s.db.Save(s.testConv).Error)
|
|
|
|
recent := &model.Conversation{AccountID: s.testAccount.ID, InboxID: s.testInbox.ID, ContactID: s.testContact.ID, Status: "open", LastActivityAt: &recentActivity, ChannelType: "web_widget", Channel: "web_widget"}
|
|
s.Require().NoError(s.db.Create(recent).Error)
|
|
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{"attribute_key": "last_activity_at", "filter_operator": "days_before", "values": []string{"7"}},
|
|
},
|
|
})
|
|
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"`
|
|
}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), int64(1), resp.Meta.AllCount)
|
|
s.Require().Len(resp.Payload, 1)
|
|
assert.Equal(s.T(), float64(s.testConv.ID), resp.Payload[0]["id"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadInvalidQueryOperator() {
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "status",
|
|
"filter_operator": "equal_to",
|
|
"values": []string{"open"},
|
|
"query_operator": "XOR",
|
|
},
|
|
},
|
|
})
|
|
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.StatusUnprocessableEntity, w.Code)
|
|
var resp map[string]string
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), "Query operator must be either \"AND\" or \"OR\".", resp["error"])
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadInvalidAttribute() {
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "phone_number",
|
|
"filter_operator": "equal_to",
|
|
"values": []string{"open"},
|
|
},
|
|
},
|
|
})
|
|
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.StatusUnprocessableEntity, w.Code)
|
|
var resp map[string]string
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Contains(s.T(), resp["error"], "Invalid attribute key - [phone_number]")
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestFilter_ChatwootPayloadInvalidOperator() {
|
|
body, _ := json.Marshal(map[string]interface{}{
|
|
"payload": []map[string]interface{}{
|
|
{
|
|
"attribute_key": "status",
|
|
"filter_operator": "eq",
|
|
"values": []string{"open"},
|
|
},
|
|
},
|
|
})
|
|
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.StatusUnprocessableEntity, w.Code)
|
|
var resp map[string]string
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), "Invalid operator. The allowed operators for status are [equal_to,not_equal_to].", resp["error"])
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestTogglePriority_EmptyBodyClearsPriorityLikeChatwoot() {
|
|
s.Require().NoError(s.db.Model(&model.Conversation{}).Where("id = ?", s.testConv.ID).Update("priority", "urgent").Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_priority", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
|
|
var conversation model.Conversation
|
|
s.Require().NoError(s.db.First(&conversation, s.testConv.ID).Error)
|
|
assert.Equal(s.T(), "none", conversation.Priority)
|
|
}
|
|
|
|
func (s *ConversationCrudTestSuite) TestTogglePriority_InvalidJSON() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", s.convURL(s.testConv.ID)+"/toggle_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)
|
|
}
|
|
|
|
// ========== 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))
|
|
}
|