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