Files
gochat/internal/handler/api/v1/assignable_agent_handler_test.go
T

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))
}