374 lines
14 KiB
Go
374 lines
14 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/suite"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
"github.com/gochat/gochat/internal/autoassignment"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/repository"
|
|
"github.com/gochat/gochat/internal/service"
|
|
)
|
|
|
|
// AssignmentPolicyHandlerTestSuite tests the AssignmentPolicyHandler endpoints
|
|
// using a real SQLite in-memory database, real repo/service, and httptest.
|
|
type AssignmentPolicyHandlerTestSuite struct {
|
|
suite.Suite
|
|
router *gin.Engine
|
|
handler *AssignmentPolicyHandler
|
|
db *gorm.DB
|
|
svc *service.AssignmentPolicyService
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) SetupSuite() {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
|
s.Require().NoError(err)
|
|
s.db = db
|
|
|
|
// AutoMigrate all required models
|
|
err = db.AutoMigrate(
|
|
&model.Account{},
|
|
&model.Inbox{},
|
|
&model.AssignmentPolicy{},
|
|
&model.InboxAssignmentPolicy{},
|
|
)
|
|
s.Require().NoError(err)
|
|
|
|
// Create repositories
|
|
policyRepo := repository.NewAssignmentPolicyRepo(db)
|
|
inboxPolicyRepo := repository.NewInboxAssignmentPolicyRepo(db)
|
|
// AssignmentService requires Redis; for handler tests that only exercise
|
|
// CRUD endpoints (Get/Create/Update/Delete), assignSvc is not invoked,
|
|
// so we pass nil. AssignConversation endpoint is not tested here.
|
|
assignSvc := (*autoassignment.AssignmentService)(nil)
|
|
|
|
// Create service
|
|
s.svc = service.NewAssignmentPolicyService(policyRepo, inboxPolicyRepo, assignSvc)
|
|
|
|
// Create handler
|
|
s.handler = NewAssignmentPolicyHandler(s.svc)
|
|
|
|
// Setup router with account-scoped assignment policy routes.
|
|
// The handler uses getAccountID which reads :account_id URL param or
|
|
// X-Account-ID header or gin context "account_id" key.
|
|
s.router = gin.New()
|
|
|
|
// mockAuthMiddleware sets account_id in gin context so getAccountID works
|
|
authMiddleware := func(c *gin.Context) {
|
|
c.Set("account_id", uint(1))
|
|
c.Next()
|
|
}
|
|
|
|
rg := s.router.Group("/api/v1/accounts/:account_id", authMiddleware)
|
|
{
|
|
rg.GET("/assignment_policy", s.handler.GetAccountPolicy)
|
|
rg.POST("/assignment_policy", s.handler.CreateAccountPolicy)
|
|
rg.PUT("/assignment_policy/:policy_id", s.handler.UpdateAccountPolicy)
|
|
rg.DELETE("/assignment_policy/:policy_id", s.handler.DeleteAccountPolicy)
|
|
rg.GET("/assignment_policies", s.handler.ListAccountPolicies)
|
|
rg.POST("/assignment_policies", s.handler.CreateAccountPolicy)
|
|
rg.GET("/assignment_policies/:policy_id", s.handler.GetAccountPolicy)
|
|
rg.PATCH("/assignment_policies/:policy_id", s.handler.UpdateAccountPolicy)
|
|
rg.GET("/assignment_policies/:policy_id/inboxes", s.handler.ListPolicyInboxes)
|
|
rg.POST("/inboxes/:inbox_id/assignment_policy", s.handler.CreateInboxPolicy)
|
|
rg.GET("/inboxes/:inbox_id/assignment_policy", s.handler.GetInboxPolicy)
|
|
rg.DELETE("/inboxes/:inbox_id/assignment_policy", s.handler.DeleteCurrentInboxPolicy)
|
|
}
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TearDownSuite() {
|
|
if s.db != nil {
|
|
sqlDB, _ := s.db.DB()
|
|
sqlDB.Close()
|
|
}
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) SetupTest() {
|
|
// Seed: create a prerequisite Account so foreign key constraints are satisfied.
|
|
s.db.Create(&model.Account{Name: "APTestOrg", Locale: "en", Active: true})
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TearDownTest() {
|
|
// Hard cleanup using DELETE (not GORM soft-delete) to avoid FK/stale-data issues.
|
|
s.db.Exec("DELETE FROM inbox_assignment_policies")
|
|
s.db.Exec("DELETE FROM assignment_policies")
|
|
s.db.Exec("DELETE FROM accounts")
|
|
}
|
|
|
|
func TestAssignmentPolicyHandlerTestSuite(t *testing.T) {
|
|
suite.Run(t, new(AssignmentPolicyHandlerTestSuite))
|
|
}
|
|
|
|
// ==================== GetAccountPolicy ====================
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_Success() {
|
|
// Create a policy via the service (since the POST endpoint also tests creation, use DB directly for setup)
|
|
policy := &model.AssignmentPolicy{
|
|
AccountID: 1,
|
|
Name: "Default",
|
|
AssignmentOrder: 0,
|
|
ConversationPriority: 1,
|
|
FairDistributionLimit: 5,
|
|
FairDistributionWindow: 300,
|
|
Enabled: true,
|
|
}
|
|
s.Require().NoError(s.db.Create(policy).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/assignment_policy", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), "Default", resp["name"])
|
|
assert.Equal(s.T(), float64(1), resp["conversation_priority"])
|
|
assert.Equal(s.T(), true, resp["enabled"])
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_NotFound() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/assignment_policy", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// service returns "not found" error → handleServiceError maps to 404
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(s.T(), resp["success"].(bool))
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestGetAccountPolicy_Unauthorized() {
|
|
// Use a separate router without auth middleware to simulate accountID == 0
|
|
unauthRouter := gin.New()
|
|
unauthRouter.GET("/api/v1/accounts/:account_id/assignment_policy", s.handler.GetAccountPolicy)
|
|
|
|
w := httptest.NewRecorder()
|
|
// non-numeric account_id param → getAccountID returns 0 → 401
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/assignment_policy", nil)
|
|
unauthRouter.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusUnauthorized, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(s.T(), resp["success"].(bool))
|
|
}
|
|
|
|
// ==================== CreateAccountPolicy ====================
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_Success() {
|
|
body := `{"name":"Default","assignment_order":0,"conversation_priority":1,"fair_distribution_limit":5,"fair_distribution_window":300,"enabled":true}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/assignment_policy", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), "Default", resp["name"])
|
|
assert.Equal(s.T(), float64(5), resp["fair_distribution_limit"])
|
|
assert.Equal(s.T(), float64(300), resp["fair_distribution_window"])
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_InvalidJSON() {
|
|
body := `{invalid json`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/assignment_policy", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(s.T(), resp["success"].(bool))
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_MissingRequiredFields() {
|
|
// ShouldBindJSON does NOT trigger validate:"required" tags.
|
|
// The service layer uses pkgvalidator.ValidateStruct which does enforce them.
|
|
// Sending empty policy field → service validation fails → handleServiceError maps to 400 (contains "required" or "oneof")
|
|
body := `{}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/assignment_policy", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// Service validation error contains "required" → handleServiceError maps to VALIDATION_ERROR / 400
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.False(s.T(), resp["success"].(bool))
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestCreateAccountPolicy_Unauthorized() {
|
|
unauthRouter := gin.New()
|
|
unauthRouter.POST("/api/v1/accounts/:account_id/assignment_policy", s.handler.CreateAccountPolicy)
|
|
|
|
body := `{"name":"Default"}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/assignment_policy", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
unauthRouter.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusUnauthorized, w.Code)
|
|
}
|
|
|
|
// ==================== UpdateAccountPolicy ====================
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_Success() {
|
|
// Create a policy first via DB
|
|
policy := &model.AssignmentPolicy{
|
|
AccountID: 1,
|
|
Name: "Default",
|
|
FairDistributionLimit: 5,
|
|
FairDistributionWindow: 300,
|
|
Enabled: true,
|
|
}
|
|
s.Require().NoError(s.db.Create(policy).Error)
|
|
|
|
body := `{"name":"Priority","fair_distribution_limit":10}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/assignment_policy/"+uintToStr(policy.ID), bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
var resp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
|
assert.Equal(s.T(), "Priority", resp["name"])
|
|
assert.Equal(s.T(), float64(10), resp["fair_distribution_limit"])
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_NotFound() {
|
|
body := `{"name":"Default"}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/assignment_policy/9999", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// "not found" → handleServiceError → 404
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_InvalidJSON() {
|
|
body := `{invalid`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("PUT", "/api/v1/accounts/1/assignment_policy/1", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
|
|
}
|
|
|
|
// ==================== DeleteAccountPolicy ====================
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_Success() {
|
|
policy := &model.AssignmentPolicy{
|
|
AccountID: 1,
|
|
Name: "Default",
|
|
FairDistributionLimit: 5,
|
|
FairDistributionWindow: 300,
|
|
Enabled: true,
|
|
}
|
|
s.Require().NoError(s.db.Create(policy).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/assignment_policy/"+uintToStr(policy.ID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestPluralAssignmentPolicies_ChatwootPayloads() {
|
|
policy := &model.AssignmentPolicy{AccountID: 1, Name: "Balanced", Description: "Route VIP first", AssignmentOrder: 1, ConversationPriority: 1, FairDistributionLimit: 20, FairDistributionWindow: 1800, Enabled: true}
|
|
s.Require().NoError(s.db.Create(policy).Error)
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/1/assignment_policies", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
var list []map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &list))
|
|
s.Require().Len(list, 1)
|
|
assert.Equal(s.T(), "Balanced", list[0]["name"])
|
|
assert.Contains(s.T(), list[0], "assigned_inbox_count")
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestInboxAssignmentPolicy_ChatwootRoutes() {
|
|
policy := &model.AssignmentPolicy{AccountID: 1, Name: "Default", Enabled: true}
|
|
s.Require().NoError(s.db.Create(policy).Error)
|
|
inbox := &model.Inbox{AccountID: 1, Name: "Support", ChannelType: "web_widget"}
|
|
s.Require().NoError(s.db.Create(inbox).Error)
|
|
|
|
body := `{"assignment_policy_id":` + uintToStr(policy.ID) + `}`
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", bytes.NewBufferString(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
var show map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &show))
|
|
assert.Equal(s.T(), "Default", show["name"])
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("GET", "/api/v1/accounts/1/assignment_policies/"+uintToStr(policy.ID)+"/inboxes", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
var inboxes map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &inboxes))
|
|
s.Require().Len(inboxes["inboxes"], 1)
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest("DELETE", "/api/v1/accounts/1/inboxes/"+uintToStr(inbox.ID)+"/assignment_policy", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
assert.Equal(s.T(), http.StatusOK, w.Code)
|
|
}
|
|
|
|
func (s *AssignmentPolicyHandlerTestSuite) TestDeleteAccountPolicy_NotFound() {
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("DELETE", "/api/v1/accounts/1/assignment_policy/9999", nil)
|
|
s.router.ServeHTTP(w, req)
|
|
|
|
// "not found" → handleServiceError → 404
|
|
assert.Equal(s.T(), http.StatusNotFound, w.Code)
|
|
}
|
|
|
|
// ==================== AssignBestAgent ====================
|
|
// Note: The AssignmentPolicyHandler does NOT have an AssignBestAgent method.
|
|
// The service has AssignConversation which calls autoassignment.AssignmentService
|
|
// (requires Redis). Since there is no handler endpoint for this, we skip the test
|
|
// and document this finding.
|
|
|
|
// ==================== Helpers ====================
|
|
|
|
// uintToStr converts a uint to string for URL path construction.
|
|
func uintToStr(id uint) string {
|
|
return strconv.FormatUint(uint64(id), 10)
|
|
}
|