Files
gochat/internal/handler/api/v1/assignment_policy_handler_test.go
T
2026-06-04 15:44:48 +08:00

320 lines
11 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{},
&autoassignment.AssignmentPolicy{},
&autoassignment.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)
}
}
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 := &autoassignment.AssignmentPolicy{
AccountID: 1,
Policy: autoassignment.PolicyRoundRobin,
FairDistributionLimit: 5,
FairDistributionWindow: 300,
Active: 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.True(s.T(), resp["success"].(bool))
data := resp["data"].(map[string]interface{})
assert.Equal(s.T(), float64(1), data["account_id"])
assert.Equal(s.T(), "round_robin", data["policy"])
}
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 := `{"policy":"round_robin","fair_distribution_limit":5,"fair_distribution_window":300,"active":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.StatusCreated, w.Code)
var resp map[string]interface{}
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
assert.True(s.T(), resp["success"].(bool))
data := resp["data"].(map[string]interface{})
assert.Equal(s.T(), float64(1), data["account_id"])
assert.Equal(s.T(), "round_robin", data["policy"])
assert.Equal(s.T(), float64(5), data["fair_distribution_limit"])
assert.Equal(s.T(), float64(300), data["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 := `{"policy":"round_robin"}`
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 := &autoassignment.AssignmentPolicy{
AccountID: 1,
Policy: autoassignment.PolicyRoundRobin,
FairDistributionLimit: 5,
FairDistributionWindow: 300,
Active: true,
}
s.Require().NoError(s.db.Create(policy).Error)
body := `{"policy":"longest_waiting","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.True(s.T(), resp["success"].(bool))
data := resp["data"].(map[string]interface{})
assert.Equal(s.T(), "longest_waiting", data["policy"])
assert.Equal(s.T(), float64(10), data["fair_distribution_limit"])
}
func (s *AssignmentPolicyHandlerTestSuite) TestUpdateAccountPolicy_NotFound() {
body := `{"policy":"round_robin"}`
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 := &autoassignment.AssignmentPolicy{
AccountID: 1,
Policy: autoassignment.PolicyRoundRobin,
FairDistributionLimit: 5,
FairDistributionWindow: 300,
Active: 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.StatusNoContent, 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)
}