698 lines
24 KiB
Go
698 lines
24 KiB
Go
package v1
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
// AssignableAgentHandlerTestSuite tests AssignableAgentHandler.List
|
|
// with a real SQLite database and wired repos → services → handler.
|
|
type AssignableAgentHandlerTestSuite struct {
|
|
suite.Suite
|
|
|
|
db *gorm.DB
|
|
router *gin.Engine
|
|
handler *AssignableAgentHandler
|
|
|
|
account *model.Account
|
|
inbox1 *model.Inbox
|
|
inbox2 *model.Inbox
|
|
inbox3 *model.Inbox
|
|
user1 *model.User // agent in inbox1
|
|
user2 *model.User // agent in inbox1 + inbox2
|
|
user3 *model.User // administrator of account
|
|
user4 *model.User // agent only in inbox2
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) SetupSuite() {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
s.Require().NoError(err, "failed to open SQLite test database")
|
|
|
|
s.Require().NoError(db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Inbox{},
|
|
&model.InboxMember{},
|
|
), "failed to auto-migrate models")
|
|
|
|
s.db = db
|
|
|
|
// Wire repos → services → handler
|
|
inboxMemberRepo := repository.NewInboxMemberRepo(db)
|
|
userRepo := repository.NewUserRepo(db)
|
|
accountRepo := repository.NewAccountRepo(db)
|
|
conversationRepo := repository.NewConversationRepo(db)
|
|
svc := service.NewAssignableAgentService(inboxMemberRepo, userRepo, accountRepo, conversationRepo)
|
|
s.handler = NewAssignableAgentHandler(svc)
|
|
|
|
// Setup router
|
|
r := gin.New()
|
|
s.router = r
|
|
|
|
// Register route matching the handler's expected URL pattern
|
|
accountGroup := r.Group("/api/v1/accounts/:account_id")
|
|
{
|
|
inboxes := accountGroup.Group("/inboxes/:inbox_id")
|
|
{
|
|
inboxes.GET("/assignable_agents", s.handler.List)
|
|
}
|
|
}
|
|
|
|
// Create test data
|
|
s.setupTestData()
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) setupTestData() {
|
|
// Create account
|
|
account := &model.Account{Name: "TestOrg", Locale: "en", Active: true}
|
|
s.Require().NoError(s.db.Create(account).Error)
|
|
s.account = account
|
|
|
|
// Create users
|
|
user1 := &model.User{AccountID: account.ID, Name: "Agent One", Email: "agent1@test.com", Role: "agent", Active: true}
|
|
s.Require().NoError(s.db.Create(user1).Error)
|
|
s.user1 = user1
|
|
|
|
user2 := &model.User{AccountID: account.ID, Name: "Agent Two", Email: "agent2@test.com", Role: "agent", Active: true}
|
|
s.Require().NoError(s.db.Create(user2).Error)
|
|
s.user2 = user2
|
|
|
|
user3 := &model.User{AccountID: account.ID, Name: "Admin Three", Email: "admin3@test.com", Role: "administrator", Active: true}
|
|
s.Require().NoError(s.db.Create(user3).Error)
|
|
s.user3 = user3
|
|
|
|
user4 := &model.User{AccountID: account.ID, Name: "Agent Four", Email: "agent4@test.com", Role: "agent", Active: true}
|
|
s.Require().NoError(s.db.Create(user4).Error)
|
|
s.user4 = user4
|
|
|
|
// Create inboxes
|
|
inbox1 := &model.Inbox{AccountID: account.ID, Name: "Inbox One", ChannelType: "web_widget", ChannelID: 1}
|
|
s.Require().NoError(s.db.Create(inbox1).Error)
|
|
s.inbox1 = inbox1
|
|
|
|
inbox2 := &model.Inbox{AccountID: account.ID, Name: "Inbox Two", ChannelType: "web_widget", ChannelID: 2}
|
|
s.Require().NoError(s.db.Create(inbox2).Error)
|
|
s.inbox2 = inbox2
|
|
|
|
inbox3 := &model.Inbox{AccountID: account.ID, Name: "Inbox Three", ChannelType: "web_widget", ChannelID: 3}
|
|
s.Require().NoError(s.db.Create(inbox3).Error)
|
|
s.inbox3 = inbox3
|
|
|
|
// Create AccountUser memberships (for administrator detection)
|
|
// user1 is an agent in the account
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: user1.ID, AccountID: account.ID, Role: "agent",
|
|
}).Error)
|
|
// user2 is an agent in the account
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: user2.ID, AccountID: account.ID, Role: "agent",
|
|
}).Error)
|
|
// user3 is an administrator in the account
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: user3.ID, AccountID: account.ID, Role: "administrator",
|
|
}).Error)
|
|
// user4 is an agent in the account
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: user4.ID, AccountID: account.ID, Role: "agent",
|
|
}).Error)
|
|
|
|
// Create InboxMember associations
|
|
// user1 is member of inbox1 only
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: inbox1.ID, UserID: user1.ID, Role: "agent",
|
|
}).Error)
|
|
// user2 is member of both inbox1 and inbox2 (intersection case)
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: inbox1.ID, UserID: user2.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: inbox2.ID, UserID: user2.ID, Role: "agent",
|
|
}).Error)
|
|
// user4 is member of inbox2 only
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: inbox2.ID, UserID: user4.ID, Role: "agent",
|
|
}).Error)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TearDownSuite() {
|
|
if s.db != nil {
|
|
sqlDB, _ := s.db.DB()
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TearDownTest() {
|
|
// Clean inbox_members so per-test data doesn't pollute
|
|
s.db.Exec("DELETE FROM inbox_members")
|
|
s.db.Exec("DELETE FROM account_users")
|
|
|
|
// Re-seed base data for next test
|
|
s.reseedData()
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) decodeAssignablePayload(w *httptest.ResponseRecorder) []interface{} {
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.Require().NotContains(resp, "success")
|
|
payload, ok := resp["payload"].([]interface{})
|
|
s.Require().True(ok, "expected Chatwoot payload array, got %v", resp)
|
|
return payload
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) reseedData() {
|
|
// Re-create the AccountUser + InboxMember associations
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: s.user1.ID, AccountID: s.account.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: s.user2.ID, AccountID: s.account.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: s.user3.ID, AccountID: s.account.ID, Role: "administrator",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.AccountUser{
|
|
UserID: s.user4.ID, AccountID: s.account.ID, Role: "agent",
|
|
}).Error)
|
|
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: s.inbox1.ID, UserID: s.user1.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: s.inbox1.ID, UserID: s.user2.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: s.inbox2.ID, UserID: s.user2.ID, Role: "agent",
|
|
}).Error)
|
|
s.Require().NoError(s.db.Create(&model.InboxMember{
|
|
InboxID: s.inbox2.ID, UserID: s.user4.ID, Role: "agent",
|
|
}).Error)
|
|
}
|
|
|
|
// ===========================
|
|
// List - Success cases
|
|
// ===========================
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_SingleInbox_ReturnsInboxMembersPlusAdministrators() {
|
|
// For inbox1: members are user1, user2. Administrators: user3.
|
|
// Result should be user1, user2, user3 (3 agents).
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Should include user1, user2 (inbox members) + user3 (administrator) = 3
|
|
s.Len(data, 3)
|
|
agent := data[0].(map[string]interface{})
|
|
s.Contains(agent, "availability_status")
|
|
s.Contains(agent, "available_name")
|
|
s.Contains(agent, "auto_offline")
|
|
s.Contains(agent, "confirmed")
|
|
s.Contains(agent, "thumbnail")
|
|
s.Contains(agent, "custom_role_id")
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_InboxWithOnlyOneMember() {
|
|
// inbox2 has user2 and user4 as members, plus administrator user3.
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox2.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// user2, user4 (inbox2 members) + user3 (administrator) = 3
|
|
s.Len(data, 3)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_InboxWithNoMembers_ReturnsOnlyAdministrators() {
|
|
// inbox3 has no inbox members assigned, so only administrators should be returned.
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox3.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Only administrator user3
|
|
s.Len(data, 1)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_MultipleInboxIDsQueryParam() {
|
|
// Request inbox1 + inbox2 via query param inbox_ids[]
|
|
// Intersection of inbox1 members {user1, user2} and inbox2 members {user2, user4} = {user2}
|
|
// Plus administrator user3 = {user2, user3}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox2.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Intersection: user2. Admin: user3. Total: 2
|
|
s.Len(data, 2)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_MultipleInboxIDsQueryParams_NoIntersection() {
|
|
// Request inbox1 + inbox3 via query param. inbox3 has no members.
|
|
// Intersection of {user1, user2} ∩ {} = {} (empty).
|
|
// Plus administrator user3 = {user3}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox3.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Empty intersection + admin user3
|
|
s.Len(data, 1)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_QueryParamSameAsPrimaryInboxID_Deduplicated() {
|
|
// Pass inbox_ids[] with the same inbox ID as the URL path parameter
|
|
// The handler should skip duplicate inbox IDs (uint(id) != inboxID check)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Same as single inbox1: user1, user2, user3
|
|
s.Len(data, 3)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_MultipleAdditionalInboxIDs() {
|
|
// Pass two additional inbox IDs in query param
|
|
// inbox_ids[]=inbox2&inbox_ids[]=inbox3
|
|
// inboxIDs = [inbox1, inbox2, inbox3]
|
|
// Intersection of inbox1 {user1, user2} ∩ inbox2 {user2, user4} ∩ inbox3 {} = {} (empty because inbox3 has no members)
|
|
// Plus admin user3 = {user3}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d&inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox2.ID, s.inbox3.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
s.Len(data, 1)
|
|
}
|
|
|
|
// ===========================
|
|
// List - Error cases
|
|
// ===========================
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_InvalidAccountID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/abc/inboxes/%d/assignable_agents", s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("BAD_REQUEST", errBody["code"])
|
|
s.Equal("invalid account_id", errBody["message"])
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_InvalidInboxID() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/notanumber/assignable_agents", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("BAD_REQUEST", errBody["code"])
|
|
s.Equal("invalid inbox_id", errBody["message"])
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_AccountIDZero() {
|
|
// account_id=0 should be rejected by handler — Chatwoot requires valid account context
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/0/inboxes/%d/assignable_agents", s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_InvalidQueryParamInboxIDs_Ignored() {
|
|
// Pass non-numeric value in inbox_ids[] - parseErr != nil, so it's skipped
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=notanumber",
|
|
s.account.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Same as just inbox1: user1, user2, user3
|
|
s.Len(data, 3)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_NonExistentInboxID() {
|
|
// inbox_id that doesn't exist - no inbox members for that inbox
|
|
// service will return empty intersection + administrators
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/99999/assignable_agents", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// No inbox members, only administrators: user3
|
|
s.Len(data, 1)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_NonExistentAccountID() {
|
|
// account_id that doesn't exist - no account users for that account (no administrators)
|
|
// But inbox member lookup is by inboxID alone, so members of inbox1 still found
|
|
// Result: inbox members {user1, user2} + no admins = {user1, user2}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/99999/inboxes/%d/assignable_agents", s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Inbox members of inbox1: user1, user2. No admins for account 99999.
|
|
s.Len(data, 2)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_BothAccountAndInboxInvalid() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
"/api/v1/accounts/abc/inboxes/xyz/assignable_agents", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("BAD_REQUEST", errBody["code"])
|
|
s.Equal("invalid account_id", errBody["message"])
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_ServiceError_ReturnsInternalServerError() {
|
|
// Create a separate DB, close it, wire handler with it to trigger service error
|
|
errDB, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
s.Require().NoError(err)
|
|
|
|
// Auto-migrate so repos can be constructed
|
|
s.Require().NoError(errDB.AutoMigrate(
|
|
&model.Account{},
|
|
&model.User{},
|
|
&model.AccountUser{},
|
|
&model.Inbox{},
|
|
&model.InboxMember{},
|
|
))
|
|
|
|
// Close the DB to make all subsequent queries fail
|
|
sqlDB, _ := errDB.DB()
|
|
sqlDB.Close()
|
|
|
|
// Wire repos → service → handler with the closed DB
|
|
inboxMemberRepo := repository.NewInboxMemberRepo(errDB)
|
|
userRepo := repository.NewUserRepo(errDB)
|
|
accountRepo := repository.NewAccountRepo(errDB)
|
|
conversationRepo := repository.NewConversationRepo(errDB)
|
|
errSvc := service.NewAssignableAgentService(inboxMemberRepo, userRepo, accountRepo, conversationRepo)
|
|
errHandler := NewAssignableAgentHandler(errSvc)
|
|
|
|
// Create a separate router for the error handler test
|
|
r := gin.New()
|
|
r.Use(gin.Recovery())
|
|
accountGroup := r.Group("/api/v1/accounts/:account_id")
|
|
{
|
|
inboxes := accountGroup.Group("/inboxes/:inbox_id")
|
|
{
|
|
inboxes.GET("/assignable_agents", errHandler.List)
|
|
}
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
// Service error is handled by handleServiceError which maps to 500 for general errors
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("INTERNAL_ERROR", errBody["code"])
|
|
}
|
|
|
|
// ===========================
|
|
// List - Edge cases
|
|
// ===========================
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_NilService_PanicRecovered() {
|
|
// Handler with nil service - calling h.svc.FindAssignableAgents causes nil pointer dereference
|
|
// Gin in TestMode doesn't recover panics by default. We need to test this gracefully.
|
|
// Since a nil service should never be constructed in production code, we test that
|
|
// the handler constructor accepts nil but document that it's invalid.
|
|
nilHandler := NewAssignableAgentHandler(nil)
|
|
|
|
// Create a separate router with recovery middleware for nil handler test
|
|
r := gin.New()
|
|
r.Use(gin.Recovery()) // Add recovery middleware to catch panics
|
|
r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/assignable_agents", nilHandler.List)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
|
|
r.ServeHTTP(w, req)
|
|
// With Recovery middleware, Gin catches the panic and returns 500
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_ResponseStructure() {
|
|
// Verify the response structure matches Chatwoot: {payload: [...]}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
|
|
s.NotContains(resp, "success")
|
|
s.NotContains(resp, "data")
|
|
s.NotNil(resp["payload"])
|
|
// No "meta" key for non-paginated response
|
|
s.Nil(resp["meta"])
|
|
// No "error" key for success response
|
|
s.Nil(resp["error"])
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_AdministratorDeduplication() {
|
|
// If an administrator is also an inbox member, they should appear only once (deduplication)
|
|
// Make user3 (administrator) also a member of inbox1
|
|
s.db.Create(&model.InboxMember{
|
|
InboxID: s.inbox1.ID, UserID: s.user3.ID, Role: "agent",
|
|
})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// user1, user2 (inbox members), user3 (admin AND inbox member) - deduplicated
|
|
// Should be 3, not 4 (user3 counted once)
|
|
s.Len(data, 3)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_AccountWithNoAdministrators() {
|
|
// Clean account_users and only add agents (no administrators)
|
|
s.db.Exec("DELETE FROM account_users")
|
|
s.db.Exec("DELETE FROM inbox_members")
|
|
|
|
// Add agents only (no administrator)
|
|
s.db.Create(&model.AccountUser{UserID: s.user1.ID, AccountID: s.account.ID, Role: "agent"})
|
|
s.db.Create(&model.AccountUser{UserID: s.user2.ID, AccountID: s.account.ID, Role: "agent"})
|
|
// Re-add inbox member for user1 in inbox1
|
|
s.db.Create(&model.InboxMember{InboxID: s.inbox1.ID, UserID: s.user1.ID, Role: "agent"})
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents", s.account.ID, s.inbox1.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
// Only inbox member user1 (no administrators added)
|
|
s.Len(data, 1)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_LargeAccountID() {
|
|
// Use a very large numeric account_id
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
"/api/v1/accounts/4294967295/inboxes/1/assignable_agents", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
s.decodeAssignablePayload(w)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_NegativeAccountID_ParseError() {
|
|
// Negative number - strconv.ParseUint fails
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
"/api/v1/accounts/-1/inboxes/1/assignable_agents", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("BAD_REQUEST", errBody["code"])
|
|
s.Equal("invalid account_id", errBody["message"])
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_NegativeInboxID_ParseError() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/-1/assignable_agents", s.account.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
s.False(resp["success"].(bool))
|
|
|
|
errBody := resp["error"].(map[string]interface{})
|
|
s.Equal("BAD_REQUEST", errBody["code"])
|
|
s.Equal("invalid inbox_id", errBody["message"])
|
|
}
|
|
|
|
// ===========================
|
|
// NewAssignableAgentHandler
|
|
// ===========================
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestNewAssignableAgentHandler() {
|
|
// Verify handler construction
|
|
inboxMemberRepo := repository.NewInboxMemberRepo(s.db)
|
|
userRepo := repository.NewUserRepo(s.db)
|
|
accountRepo := repository.NewAccountRepo(s.db)
|
|
conversationRepo := repository.NewConversationRepo(s.db)
|
|
svc := service.NewAssignableAgentService(inboxMemberRepo, userRepo, accountRepo, conversationRepo)
|
|
handler := NewAssignableAgentHandler(svc)
|
|
s.NotNil(handler)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestNewAssignableAgentHandler_NilService() {
|
|
handler := NewAssignableAgentHandler(nil)
|
|
s.NotNil(handler)
|
|
// Handler exists but svc is nil - will panic on use
|
|
}
|
|
|
|
// ===========================
|
|
// Cross-inbox intersection scenarios
|
|
// ===========================
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_TwoInboxesWithPartialIntersection() {
|
|
// inbox1 has {user1, user2}, inbox2 has {user2, user4}
|
|
// Intersection = {user2}
|
|
// Plus admin user3
|
|
// Result: user2, user3 (2 agents)
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox2.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
s.Len(data, 2)
|
|
}
|
|
|
|
func (s *AssignableAgentHandlerTestSuite) TestList_ThreeInboxesIntersectionWithTwo() {
|
|
// Create a scenario where user2 is in inbox1, inbox2, and inbox3
|
|
s.db.Create(&model.InboxMember{InboxID: s.inbox3.ID, UserID: s.user2.ID, Role: "agent"})
|
|
|
|
// Request inbox1 + inbox2 + inbox3
|
|
// Intersection of inbox1{user1,user2} ∩ inbox2{user2,user4} ∩ inbox3{user2} = {user2}
|
|
// Plus admin user3 = {user2, user3}
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET",
|
|
fmt.Sprintf("/api/v1/accounts/%d/inboxes/%d/assignable_agents?inbox_ids[]=%d&inbox_ids[]=%d",
|
|
s.account.ID, s.inbox1.ID, s.inbox2.ID, s.inbox3.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
data := s.decodeAssignablePayload(w)
|
|
s.Len(data, 2)
|
|
}
|
|
|
|
// Run the test suite
|
|
func TestAssignableAgentHandlerSuite(t *testing.T) {
|
|
suite.Run(t, new(AssignableAgentHandlerTestSuite))
|
|
}
|