package v1 import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "regexp" "testing" "github.com/gin-gonic/gin" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" pkgcrypto "github.com/gochat/gochat/pkg/crypto" "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{}, &model.UserSession{}, &model.Audit{}, &model.InstallationConfig{})) s.db = db agentRepo := repository.NewAgentRepo(db) svc := service.NewAgentService(agentRepo, db) s.handler = NewAgentHandler(svc).WithAuditService(service.NewAuditService(repository.NewAuditRepo(db))) 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") s.db.Exec("DELETE FROM user_sessions") s.db.Exec("DELETE FROM audits") // 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 { 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 data []interface{} assert.NoError(s.T(), json.Unmarshal(w.Body.Bytes(), &data)) assert.Equal(s.T(), 0, len(data)) } func (s *AgentHandlerTestSuite) TestListOrdersByFullName() { agents := []service.CreateAgentRequest{ {Email: "charlie@test.com", Name: "charlie", Role: "agent"}, {Email: "alpha@test.com", Name: "Alpha", Role: "agent"}, {Email: "bravo@test.com", Name: "bravo", Role: "agent"}, } for _, req := range agents { 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.StatusOK, w.Code) } 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 data []map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } s.Require().Len(data, 3) assert.Equal(s.T(), "Alpha", data[0]["name"]) assert.Equal(s.T(), "bravo", data[1]["name"]) assert.Equal(s.T(), "charlie", data[2]["name"]) } func (s *AgentHandlerTestSuite) TestListIgnoresPerPageLikeChatwoot() { for i := 0; i < 30; i++ { req := service.CreateAgentRequest{ Email: fmt.Sprintf("full-list-%02d@test.com", i), Name: fmt.Sprintf("Full List %02d", 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.StatusOK, w.Code) } w, c := s.makeRequest("GET", "/api/v1/accounts/1/agents?per_page=5", nil, s.account.ID, s.user.ID) s.handler.List(c) assert.Equal(s.T(), http.StatusOK, w.Code) var data []map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } s.Require().Len(data, 30) assert.Equal(s.T(), "Full List 00", data[0]["name"]) assert.Equal(s.T(), "Full List 29", data[29]["name"]) } func (s *AgentHandlerTestSuite) TestCreateAgent() { customRoleID := uint(7) s.Require().NoError(s.db.Create(&model.CustomRole{ID: customRoleID, AccountID: s.account.ID, Name: "Custom", Permissions: "[]"}).Error) req := service.CreateAgentRequest{ Email: "agent1@test.com", Name: "Agent One", Role: "agent", CustomRoleID: &customRoleID, } 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.StatusOK, w.Code) var data map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } assert.Equal(s.T(), "agent1@test.com", data["email"]) assert.Equal(s.T(), "Agent One", data["name"]) assert.Equal(s.T(), "offline", data["availability_status"]) assert.Equal(s.T(), float64(7), data["custom_role_id"]) assert.Contains(s.T(), data, "confirmed") assert.NotContains(s.T(), data, "invited_by") assert.NotContains(s.T(), data, "inviter_id") assert.NotContains(s.T(), data, "account_user_id") temporaryPassword, ok := data["temporary_password"].(string) s.Require().True(ok) assert.Len(s.T(), temporaryPassword, 16) assert.Equal(s.T(), "no-store", w.Header().Get("Cache-Control")) var membership model.AccountUser s.Require().NoError(s.db.Where("account_id = ? AND user_id = ?", s.account.ID, uint(data["id"].(float64))).First(&membership).Error) assert.Equal(s.T(), s.user.ID, membership.InvitedBy) var createdUser model.User s.Require().NoError(s.db.First(&createdUser, uint(data["id"].(float64))).Error) assert.NotNil(s.T(), createdUser.ConfirmedAt) assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, createdUser.PasswordDigest)) assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, createdUser.Password)) } func (s *AgentHandlerTestSuite) TestCreateAgentChatwootFrontendPayload() { req := service.CreateAgentRequest{ Email: "chatwoot-agent@test.com", Name: "Chatwoot Agent", Role: "administrator", } 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.StatusOK, w.Code) var data map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) assert.Equal(s.T(), "chatwoot-agent@test.com", data["email"]) assert.Equal(s.T(), "Chatwoot Agent", data["name"]) assert.Equal(s.T(), "Chatwoot Agent", data["available_name"]) assert.Equal(s.T(), "administrator", data["role"]) assert.Equal(s.T(), "offline", data["availability_status"]) assert.IsType(s.T(), true, data["auto_offline"]) assert.Equal(s.T(), "email", data["provider"]) assert.Contains(s.T(), data, "account_id") assert.Contains(s.T(), data, "confirmed") assert.Contains(s.T(), data, "thumbnail") assert.NotContains(s.T(), data, "payload") assert.NotContains(s.T(), data, "data") } func (s *AgentHandlerTestSuite) TestCreateAgentDoesNotRequireInvitationEmail() { req := service.CreateAgentRequest{Email: "direct-login@test.com", Name: "Direct Login", 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.StatusOK, w.Code, w.Body.String()) var data map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) temporaryPassword, ok := data["temporary_password"].(string) s.Require().True(ok) assert.Regexp(s.T(), `[A-Z]`, temporaryPassword) assert.Regexp(s.T(), `[a-z]`, temporaryPassword) assert.Regexp(s.T(), `[0-9]`, temporaryPassword) assert.Regexp(s.T(), `[!@#$%]`, temporaryPassword) var created model.User s.Require().NoError(s.db.Where("email = ?", "direct-login@test.com").First(&created).Error) assert.Empty(s.T(), created.ResetPasswordToken) assert.Nil(s.T(), created.ResetPasswordSentAt) assert.NotNil(s.T(), created.ConfirmedAt) assert.True(s.T(), pkgcrypto.CheckPassword(temporaryPassword, created.PasswordDigest)) } func (s *AgentHandlerTestSuite) TestResetPasswordReturnsNewTemporaryPassword() { req := service.CreateAgentRequest{Email: "reset-agent@test.com", Name: "Reset Agent", Role: "agent"} createResponse, createContext := s.makeRequest("POST", "/api/v1/accounts/1/agents", req, s.account.ID, s.user.ID) s.handler.Create(createContext) s.Require().Equal(http.StatusOK, createResponse.Code) var created map[string]interface{} s.Require().NoError(json.Unmarshal(createResponse.Body.Bytes(), &created)) agentID := uint(created["id"].(float64)) initialPassword := created["temporary_password"].(string) w, c := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/1/agents/%d/reset_password", agentID), nil, s.account.ID, s.user.ID) s.handler.ResetPassword(c) assert.Equal(s.T(), http.StatusOK, w.Code, w.Body.String()) assert.Equal(s.T(), "no-store", w.Header().Get("Cache-Control")) var data map[string]string s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) newPassword := data["temporary_password"] assert.Len(s.T(), newPassword, 16) assert.NotEqual(s.T(), initialPassword, newPassword) var user model.User s.Require().NoError(s.db.First(&user, agentID).Error) assert.True(s.T(), pkgcrypto.CheckPassword(newPassword, user.PasswordDigest)) assert.False(s.T(), pkgcrypto.CheckPassword(initialPassword, user.PasswordDigest)) } func (s *AgentHandlerTestSuite) TestResetPasswordIsScopedToAccount() { otherAccount := &model.Account{Name: "other-reset-account"} s.Require().NoError(s.db.Create(otherAccount).Error) w, c := s.makeRequest("POST", fmt.Sprintf("/api/v1/accounts/%d/agents/%d/reset_password", otherAccount.ID, s.user.ID), nil, otherAccount.ID, s.user.ID) s.handler.ResetPassword(c) assert.Equal(s.T(), http.StatusNotFound, w.Code, w.Body.String()) } func (s *AgentHandlerTestSuite) TestCreateAgentDefaultsBlankNameFromEmail() { req := map[string]interface{}{ "agent": map[string]interface{}{ "email": "fallback-name@test.com", "name": "", }, } 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.StatusOK, w.Code, w.Body.String()) var data map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } assert.Equal(s.T(), "fallback-name@test.com", data["email"]) assert.Equal(s.T(), "fallback-name", data["name"]) var user model.User s.Require().NoError(s.db.Where("email = ?", "fallback-name@test.com").First(&user).Error) assert.Equal(s.T(), "fallback-name", user.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.StatusOK, w.Code) // Try creating again — Chatwoot renders ActiveRecord::RecordInvalid as 422. 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.StatusUnprocessableEntity, w2.Code) var data map[string]interface{} if err := json.Unmarshal(w2.Body.Bytes(), &data); err != nil { panic(err) } assert.Equal(s.T(), "User has already been taken", data["message"]) assert.Equal(s.T(), []interface{}{"user_id"}, data["attributes"]) } 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.StatusOK, w.Code) var data map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } 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 getData map[string]interface{} if err := json.Unmarshal(w2.Body.Bytes(), &getData); err != nil { panic(err) } 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", AutoOffline: true, } 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.StatusOK, w.Code) var data map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } agentID := uint(data["id"].(float64)) // Update the agent updateReq := map[string]any{ "agent": map[string]any{ "name": "Updated Name", "role": "administrator", "availability": "online", }, } w2, c2 := s.makeRequest("PATCH", 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 updateData map[string]interface{} if err := json.Unmarshal(w2.Body.Bytes(), &updateData); err != nil { panic(err) } assert.Equal(s.T(), "Updated Name", updateData["name"]) assert.Equal(s.T(), "administrator", updateData["role"]) assert.Equal(s.T(), "online", updateData["availability_status"]) assert.Equal(s.T(), true, updateData["auto_offline"]) // Explicit false should still update, matching Rails compact semantics. disableReq := map[string]any{ "agent": map[string]any{ "auto_offline": false, }, } w3, c3 := s.makeRequest("PATCH", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), disableReq, s.account.ID, s.user.ID) s.handler.Update(c3) assert.Equal(s.T(), http.StatusOK, w3.Code) var disableData map[string]interface{} if err := json.Unmarshal(w3.Body.Bytes(), &disableData); err != nil { panic(err) } assert.Equal(s.T(), false, disableData["auto_offline"]) } func (s *AgentHandlerTestSuite) TestUpdateAgentDeactivatesAndRevokesSessions() { create, createCtx := s.makeRequest("POST", "/api/v1/accounts/1/agents", service.CreateAgentRequest{Email: "inactive@test.com", Name: "Inactive Agent", Role: "agent", Availability: "online"}, s.account.ID, s.user.ID) s.handler.Create(createCtx) s.Require().Equal(http.StatusOK, create.Code) var created map[string]interface{} s.Require().NoError(json.Unmarshal(create.Body.Bytes(), &created)) agentID := uint(created["id"].(float64)) s.Require().NoError(s.db.Create(&model.UserSession{UserID: agentID, ClientID: "active-client"}).Error) otherAccount := model.Account{Name: "other membership"} s.Require().NoError(s.db.Create(&otherAccount).Error) s.Require().NoError(s.db.Create(&model.AccountUser{UserID: agentID, AccountID: otherAccount.ID, Role: "agent", Availability: "online"}).Error) update := map[string]any{"agent": map[string]any{"active": false}} w, c := s.makeRequest("PATCH", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), update, s.account.ID, s.user.ID) s.handler.Update(c) s.Equal(http.StatusOK, w.Code, w.Body.String()) var payload map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &payload)) s.Equal(false, payload["active"]) s.Equal("offline", payload["availability_status"]) var sessions int64 s.Require().NoError(s.db.Model(&model.UserSession{}).Where("user_id = ?", agentID).Count(&sessions).Error) s.Zero(sessions) var user model.User s.Require().NoError(s.db.First(&user, agentID).Error) s.False(user.Active) var memberships []model.AccountUser s.Require().NoError(s.db.Where("user_id = ?", agentID).Find(&memberships).Error) for _, membership := range memberships { s.Equal("offline", membership.Availability) } var audit model.Audit s.Require().NoError(s.db.Where("auditable_type = ? AND auditable_id = ? AND action = ?", "User", agentID, "update").First(&audit).Error) s.Contains(string(audit.AuditedChanges), `"active":false`) } func (s *AgentHandlerTestSuite) TestUpdateAgentCannotDeactivateSelf() { active := false w, c := s.makeRequest("PATCH", fmt.Sprintf("/api/v1/accounts/1/agents/%d", s.user.ID), map[string]any{"agent": map[string]any{"active": active}}, s.account.ID, s.user.ID) s.handler.Update(c) s.Equal(http.StatusUnprocessableEntity, w.Code) } func (s *AgentHandlerTestSuite) TestUpdateAgentBlankNameReturnsRecordInvalidShape() { req := service.CreateAgentRequest{ Email: "blank-update@test.com", Name: "Blank Update", 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.StatusOK, w.Code) var created map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &created); err != nil { panic(err) } agentID := uint(created["id"].(float64)) updateReq := map[string]any{ "agent": map[string]any{ "name": "", }, } w2, c2 := s.makeRequest("PATCH", fmt.Sprintf("/api/v1/accounts/1/agents/%d", agentID), updateReq, s.account.ID, s.user.ID) s.handler.Update(c2) assert.Equal(s.T(), http.StatusUnprocessableEntity, w2.Code, w2.Body.String()) var data map[string]interface{} if err := json.Unmarshal(w2.Body.Bytes(), &data); err != nil { panic(err) } assert.Equal(s.T(), "Name can't be blank", data["message"]) assert.Equal(s.T(), []interface{}{"name"}, data["attributes"]) var user model.User s.Require().NoError(s.db.First(&user, agentID).Error) assert.Equal(s.T(), "Blank Update", user.Name) } 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.StatusOK, w.Code) var data map[string]interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } 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) TestDeleteAgentNotInAccountReturnsNotFound() { otherAccount := &model.Account{Name: "Other Account", Locale: "en", Active: true} s.Require().NoError(s.db.Create(otherAccount).Error) otherUser := &model.User{Name: "Other Agent", Email: "other-agent@test.com", Provider: "email", Active: true} s.Require().NoError(s.db.Create(otherUser).Error) s.Require().NoError(s.db.Create(&model.AccountUser{AccountID: otherAccount.ID, UserID: otherUser.ID, Role: "agent", Availability: "offline", AutoOffline: true}).Error) w, c := s.makeRequest("DELETE", fmt.Sprintf("/api/v1/accounts/%d/agents/%d", s.account.ID, otherUser.ID), nil, s.account.ID, s.user.ID) s.handler.Delete(c) assert.Equal(s.T(), http.StatusNotFound, w.Code, w.Body.String()) var userCount int64 s.Require().NoError(s.db.Model(&model.User{}).Where("id = ?", otherUser.ID).Count(&userCount).Error) assert.Equal(s.T(), int64(1), userCount) var membershipCount int64 s.Require().NoError(s.db.Model(&model.AccountUser{}).Where("account_id = ? AND user_id = ?", otherAccount.ID, otherUser.ID).Count(&membershipCount).Error) assert.Equal(s.T(), int64(1), membershipCount) } 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 data []map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) s.Require().Len(data, 3) for _, agent := range data { assert.NotEmpty(s.T(), agent["temporary_password"]) } } 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.StatusOK, w.Code) assert.JSONEq(s.T(), `[]`, w.Body.String()) } func (s *AgentHandlerTestSuite) TestBulkCreateSkipsInvalidEmailsAndClearsOnboardingStep() { s.Require().NoError(s.db.Model(s.account).Update("onboarding_step", "invite_team").Error) req := service.BulkCreateAgentRequest{ Emails: []string{"valid-bulk@test.com", "invalid-email"}, } 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 data []map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &data)) s.Require().Len(data, 1) assert.NotEmpty(s.T(), data[0]["temporary_password"]) var validUser model.User s.Require().NoError(s.db.Where("email = ?", "valid-bulk@test.com").First(&validUser).Error) var invalidCount int64 s.Require().NoError(s.db.Model(&model.User{}).Where("email = ?", "invalid-email").Count(&invalidCount).Error) assert.Equal(s.T(), int64(0), invalidCount) var account model.Account s.Require().NoError(s.db.First(&account, s.account.ID).Error) assert.Equal(s.T(), "", account.OnboardingStep) } 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.StatusOK, 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 data []map[string]interface{} s.Require().NoError(json.Unmarshal(w2.Body.Bytes(), &data)) s.Require().Len(data, 1) assert.Equal(s.T(), "new@test.com", data[0]["email"]) assert.NotEmpty(s.T(), data[0]["temporary_password"]) } 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.StatusOK, 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 data []interface{} if err := json.Unmarshal(w.Body.Bytes(), &data); err != nil { panic(err) } 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) }