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) }