421 lines
14 KiB
Go
421 lines
14 KiB
Go
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"
|
|
"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 {
|
|
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/<number> or /agents/<number>/...
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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) TestCreateAgent() {
|
|
customRoleID := uint(7)
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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")
|
|
}
|
|
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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{}
|
|
json.Unmarshal(w2.Body.Bytes(), &data)
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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{}
|
|
json.Unmarshal(w2.Body.Bytes(), &getData)
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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{}
|
|
json.Unmarshal(w2.Body.Bytes(), &updateData)
|
|
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{}
|
|
json.Unmarshal(w3.Body.Bytes(), &disableData)
|
|
assert.Equal(s.T(), false, disableData["auto_offline"])
|
|
}
|
|
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
}
|
|
|
|
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.Empty(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)
|
|
assert.Empty(s.T(), w.Body.String())
|
|
|
|
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)
|
|
assert.Empty(s.T(), w2.Body.String())
|
|
}
|
|
|
|
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{}
|
|
json.Unmarshal(w.Body.Bytes(), &data)
|
|
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)
|
|
}
|