package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "regexp" "strings" "testing" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" ) type AgentHandlerTestSuite struct { suite.Suite db *gorm.DB handler *AgentHandler account *model.Account user *model.User } func (s *AgentHandlerTestSuite) 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) s.Require().NoError(db.AutoMigrate(&model.Account{}, &model.User{}, &model.AccountUser{})) s.db = db agentRepo := repository.NewAgentRepo(db) svc := service.NewAgentService(agentRepo, db) s.handler = NewAgentHandler(svc) s.account = &model.Account{Name: "test-agent-account"} s.Require().NoError(db.Create(s.account).Error) s.user = &model.User{ Name: "Inviter Admin", Email: "inviter@test.com", Provider: "email", Active: true, } s.Require().NoError(db.Create(s.user).Error) } func (s *AgentHandlerTestSuite) SetupTest() { s.db.Exec("DELETE FROM account_users") // Don't delete users — we need the inviter user to persist // Only delete agent users (not the inviter) s.db.Exec("DELETE FROM users WHERE id != ?", s.user.ID) } func (s *AgentHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } func TestAgentHandlerSuite(t *testing.T) { suite.Run(t, new(AgentHandlerTestSuite)) } func (s *AgentHandlerTestSuite) makeRequest(method, path string, body interface{}, accountID uint, userID uint) (*httptest.ResponseRecorder, *gin.Context) { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) var bodyBytes []byte if body != nil { // Chatwoot params.require(:agent) → wrap body under "agent" key for POST/PUT/PATCH // Exception: bulk_create does NOT use params.require needsWrap := (method == "POST" || method == "PUT" || method == "PATCH") && !strings.Contains(path, "bulk_create") if needsWrap { wrapped := map[string]interface{}{"agent": body} bodyBytes, _ = json.Marshal(wrapped) } else { bodyBytes, _ = json.Marshal(body) } } c.Request = httptest.NewRequest(method, path, bytes.NewReader(bodyBytes)) if body != nil { c.Request.Header.Set("Content-Type", "application/json") } // Set account_id and id params (both refer to account ID in GoChat routes) params := gin.Params{ {Key: "account_id", Value: fmt.Sprintf("%d", accountID)}, {Key: "id", Value: fmt.Sprintf("%d", accountID)}, } // Extract agent_id from path like /api/v1/accounts/1/agents/123 // Match pattern: /agents/ or /agents//... re := regexp.MustCompile(`/agents/(\d+)`) matches := re.FindStringSubmatch(path) if len(matches) > 1 { params = append(params, gin.Param{Key: "agent_id", Value: matches[1]}) } c.Params = params c.Set("account_id", accountID) c.Set("user_id", userID) return w, c } func (s *AgentHandlerTestSuite) TestListEmpty() { w, c := s.makeRequest("GET", "/api/v1/accounts/1/agents", nil, s.account.ID, s.user.ID) s.handler.List(c) 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"].([]interface{}) assert.Equal(s.T(), 0, len(data)) } func (s *AgentHandlerTestSuite) TestCreateAgent() { req := service.CreateAgentRequest{ Email: "agent1@test.com", Name: "Agent One", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) 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.Equal(s.T(), "agent1@test.com", data["email"]) assert.Equal(s.T(), "Agent One", data["name"]) } func (s *AgentHandlerTestSuite) TestCreateAgentDuplicate() { // Create first agent req := service.CreateAgentRequest{ Email: "agent2@test.com", Name: "Agent Two", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) // Try creating again — should conflict w2, c2 := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c2) assert.Equal(s.T(), http.StatusConflict, w2.Code) } func (s *AgentHandlerTestSuite) TestCreateAgentValidation() { req := service.CreateAgentRequest{ Email: "", // required Name: "", // required } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *AgentHandlerTestSuite) TestGetAgent() { // Create an agent first req := service.CreateAgentRequest{ Email: "agent3@test.com", Name: "Agent Three", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) var createResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createResp) data := createResp["data"].(map[string]interface{}) agentID := uint(data["id"].(float64)) // Get the agent w2, c2 := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), nil, s.account.ID, s.user.ID) c2.Params = append(c2.Params, gin.Param{Key: "id", Value: fmt.Sprintf("%d", agentID)}) s.handler.Get(c2) assert.Equal(s.T(), http.StatusOK, w2.Code) var getResp map[string]interface{} json.Unmarshal(w2.Body.Bytes(), &getResp) assert.True(s.T(), getResp["success"].(bool)) getData := getResp["data"].(map[string]interface{}) assert.Equal(s.T(), "agent3@test.com", getData["email"]) } func (s *AgentHandlerTestSuite) TestGetAgentNotFound() { w, c := s.makeRequest("GET", "/api/v1/accounts/1/agents/9999", nil, s.account.ID, s.user.ID) s.handler.Get(c) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *AgentHandlerTestSuite) TestUpdateAgent() { // Create an agent req := service.CreateAgentRequest{ Email: "agent4@test.com", Name: "Agent Four", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) var createResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createResp) data := createResp["data"].(map[string]interface{}) agentID := uint(data["id"].(float64)) // Update the agent updateReq := service.UpdateAgentRequest{ Name: "Updated Name", Role: "administrator", Availability: "online", } w2, c2 := s.makeRequest("PUT", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), updateReq, s.account.ID, s.user.ID) c2.Params = append(c2.Params, gin.Param{Key: "id", Value: fmt.Sprintf("%d", agentID)}) s.handler.Update(c2) assert.Equal(s.T(), http.StatusOK, w2.Code) var updateResp map[string]interface{} json.Unmarshal(w2.Body.Bytes(), &updateResp) assert.True(s.T(), updateResp["success"].(bool)) updateData := updateResp["data"].(map[string]interface{}) assert.Equal(s.T(), "Updated Name", updateData["name"]) assert.Equal(s.T(), "administrator", updateData["role"]) assert.Equal(s.T(), "online", updateData["availability"]) } func (s *AgentHandlerTestSuite) TestDeleteAgent() { // Create an agent req := service.CreateAgentRequest{ Email: "agent5@test.com", Name: "Agent Five", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) var createResp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &createResp) data := createResp["data"].(map[string]interface{}) agentID := uint(data["id"].(float64)) // Delete the agent w2, c2 := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), nil, s.account.ID, s.user.ID) c2.Params = append(c2.Params, gin.Param{Key: "id", Value: fmt.Sprintf("%d", agentID)}) s.handler.Delete(c2) assert.Equal(s.T(), http.StatusOK, w2.Code) // Verify agent is gone w3, c3 := s.makeRequest("GET", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), nil, s.account.ID, s.user.ID) c3.Params = append(c3.Params, gin.Param{Key: "id", Value: fmt.Sprintf("%d", agentID)}) s.handler.Get(c3) assert.Equal(s.T(), http.StatusNotFound, w3.Code) } func (s *AgentHandlerTestSuite) TestBulkCreate() { req := service.BulkCreateAgentRequest{ Emails: []string{"bulk1@test.com", "bulk2@test.com", "bulk3@test.com"}, } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents/bulk_create", req, s.account.ID, s.user.ID) s.handler.BulkCreate(c) 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"].([]interface{}) assert.Equal(s.T(), 3, len(data)) // Verify emails for i, agent := range data { agentMap := agent.(map[string]interface{}) expectedEmail := fmt.Sprintf("bulk%d@test.com", i+1) assert.Equal(s.T(), expectedEmail, agentMap["email"]) } } func (s *AgentHandlerTestSuite) TestBulkCreateValidation() { req := service.BulkCreateAgentRequest{ Emails: []string{}, // min=1 } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents/bulk_create", req, s.account.ID, s.user.ID) s.handler.BulkCreate(c) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *AgentHandlerTestSuite) TestBulkCreateSkipsDuplicates() { // Pre-create one agent preReq := service.CreateAgentRequest{ Email: "existing@test.com", Name: "Existing", Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", preReq, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) // Bulk create with a duplicate + a new one req := service.BulkCreateAgentRequest{ Emails: []string{"existing@test.com", "new@test.com"}, } w2, c2 := s.makeRequest("POST", "/api/v1/accounts/1/agents/bulk_create", req, s.account.ID, s.user.ID) s.handler.BulkCreate(c2) assert.Equal(s.T(), http.StatusOK, w2.Code) var resp map[string]interface{} json.Unmarshal(w2.Body.Bytes(), &resp) data := resp["data"].([]interface{}) // Only the new one should be in results (existing skipped silently) assert.Equal(s.T(), 1, len(data)) newAgent := data[0].(map[string]interface{}) assert.Equal(s.T(), "new@test.com", newAgent["email"]) } func (s *AgentHandlerTestSuite) TestListAfterCreate() { // Create two agents for i := 1; i <= 2; i++ { req := service.CreateAgentRequest{ Email: fmt.Sprintf("list%d@test.com", i), Name: fmt.Sprintf("List Agent %d", i), Role: "agent", } w, c := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(c) assert.Equal(s.T(), http.StatusCreated, w.Code) } // List agents w, c := s.makeRequest("GET", "/api/v1/accounts/1/agents", nil, s.account.ID, s.user.ID) s.handler.List(c) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) data := resp["data"].([]interface{}) assert.Equal(s.T(), 2, len(data)) } func (s *AgentHandlerTestSuite) TestListNoAccountID() { w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest("GET", "/api/v1/accounts/agents", nil) // No account_id set s.handler.List(c) assert.Equal(s.T(), http.StatusUnauthorized, w.Code) }