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