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/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // ========== Test Setup ========== func setupSlaPolicyHandlerTest(t *testing.T) (*SlaPolicyHandler, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{ Logger: logger.Default.LogMode(logger.Silent), }) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.Account{}, &model.SlaPolicy{}, &model.SlaPolicyInbox{}, &model.AppliedSLA{}, &model.SlaEvent{}, )) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) slaPolicyRepo := repository.NewSlaPolicyRepo(db) appliedSlaRepo := repository.NewAppliedSlaRepo(db) slaEventRepo := repository.NewSlaEventRepo(db) slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db) svc := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo) handler := NewSlaPolicyHandler(svc) // Seed an account for all tests account := &model.Account{Name: "SlaHandlerOrg", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) return handler, db } func createSlaHandlerTestConversation(db *gorm.DB, accountID uint) *model.Conversation { inbox := &model.Inbox{AccountID: accountID, Name: "test-inbox", ChannelType: "web_widget"} db.Create(inbox) conv := &model.Conversation{AccountID: accountID, InboxID: inbox.ID, Status: "open"} db.Create(conv) return conv } func slaHandlerAccountID(db *gorm.DB) string { var account model.Account db.First(&account) return strconv.FormatUint(uint64(account.ID), 10) } func slaHandlerAccountIDUint(db *gorm.DB) uint { var account model.Account db.First(&account) return account.ID } func setupSlaPolicyTestRouter(handler *SlaPolicyHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() rg := r.Group("/api/v1/accounts/:account_id") rg.GET("/sla_policies", handler.List) rg.POST("/sla_policies", handler.Create) rg.GET("/sla_policies/:id", handler.Get) rg.PUT("/sla_policies/:id", handler.Update) rg.DELETE("/sla_policies/:id", handler.Delete) rg.GET("/sla_policies/:id/inboxes", handler.ListInboxes) rg.POST("/sla_policies/:id/inboxes", handler.AddInbox) rg.DELETE("/sla_policies/:id/inboxes/:inbox_id", handler.RemoveInbox) rg.GET("/applied_slas/metrics", handler.GetAppliedSlaMetrics) rg.GET("/applied_slas/download", handler.GetAppliedSlaDownload) return r } // ========== List ========== func TestSlaPolicyHandler_List_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) // Seed two policies slaPolicyRepo := repository.NewSlaPolicyRepo(db) appliedSlaRepo := repository.NewAppliedSlaRepo(db) slaEventRepo := repository.NewSlaEventRepo(db) slaPolicyInboxRepo := repository.NewSlaPolicyInboxRepo(db) svc := service.NewSlaPolicyService(slaPolicyRepo, appliedSlaRepo, slaEventRepo, slaPolicyInboxRepo) svc.Create(nil, slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "Policy-A", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) svc.Create(nil, slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "Policy-B", FirstResponseTimeThreshold: 20, NextResponseTimeThreshold: 40, ResolutionTimeThreshold: 200, }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_List_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.GET("/sla_policies", handler.List) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/sla_policies", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Create ========== func TestSlaPolicyHandler_Create_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "New SLA", "description": "desc", "first_response_time_threshold": 30, "next_response_time_threshold": 60, "resolution_time_threshold": 480, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) } func TestSlaPolicyHandler_Create_ValidationError(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "", }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSlaPolicyHandler_Create_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.POST("/sla_policies", handler.Create) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "New SLA", "first_response_time_threshold": 30, "next_response_time_threshold": 60, "resolution_time_threshold": 480, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/sla_policies", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Get ========== func TestSlaPolicyHandler_Get_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "GetTest", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_Get_InvalidID(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/notanumber", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ========== Update ========== func TestSlaPolicyHandler_Update_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "ToUpdate", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "Updated Name", "first_response_time_threshold": 45, }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_Update_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.PUT("/sla_policies/:id", handler.Update) body := map[string]interface{}{ "sla_policy": map[string]interface{}{ "name": "Nope", }, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/accounts/sla_policies/1", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== Delete ========== func TestSlaPolicyHandler_Delete_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{ Name: "ToDelete", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10), nil) router.ServeHTTP(w, req) // Chatwoot returns head :ok (200) on destroy, not 204 assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_Delete_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.DELETE("/sla_policies/:id", handler.Delete) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/sla_policies/1", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== AddInbox ========== func TestSlaPolicyHandler_AddInbox_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) body := map[string]interface{}{ "inbox_id": inbox.ID, } jsonBody, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", bytes.NewBuffer(jsonBody)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusCreated, w.Code) } // ========== RemoveInbox ========== func TestSlaPolicyHandler_RemoveInbox_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) svc.AddInbox(nil, accountUID, policy.ID, inbox.ID) w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes/"+strconv.FormatUint(uint64(inbox.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNoContent, w.Code) } // ========== ListInboxes ========== func TestSlaPolicyHandler_ListInboxes_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID} require.NoError(t, db.Create(inbox).Error) svc.AddInbox(nil, accountUID, policy.ID, inbox.ID) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } // ========== GetAppliedSlaMetrics ========== func TestSlaPolicyHandler_GetAppliedSlaMetrics_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) applied := &model.AppliedSLA{ AccountID: accountUID, ConversationID: 1, SlaPolicyID: policy.ID, } require.NoError(t, db.Create(applied).Error) event := &model.SlaEvent{AppliedSlaID: applied.ID, EventType: "frt_reached"} require.NoError(t, db.Create(event).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/metrics?conversation_id=1", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestSlaPolicyHandler_GetAppliedSlaMetrics_MissingConversationID(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/metrics", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSlaPolicyHandler_GetAppliedSlaMetrics_InvalidConversationID(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/metrics?conversation_id=notanumber", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestSlaPolicyHandler_GetAppliedSlaMetrics_NoAccountID(t *testing.T) { gin.SetMode(gin.TestMode) r := gin.New() handler, _ := setupSlaPolicyHandlerTest(t) rg := r.Group("/api/v1/accounts") rg.GET("/applied_slas/metrics", handler.GetAppliedSlaMetrics) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/applied_slas/metrics?conversation_id=1", nil) r.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code) } // ========== GetAppliedSlaDownload ========== func TestSlaPolicyHandler_GetAppliedSlaDownload_Success(t *testing.T) { handler, db := setupSlaPolicyHandlerTest(t) router := setupSlaPolicyTestRouter(handler) aid := slaHandlerAccountID(db) accountUID := slaHandlerAccountIDUint(db) svc := service.NewSlaPolicyService( repository.NewSlaPolicyRepo(db), repository.NewAppliedSlaRepo(db), repository.NewSlaEventRepo(db), repository.NewSlaPolicyInboxRepo(db), ) policy, _ := svc.Create(nil, accountUID, &service.CreateSlaPolicyRequest{ Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100, }) applied := &model.AppliedSLA{ AccountID: accountUID, SlaPolicyID: policy.ID, ConversationID: createSlaHandlerTestConversation(db, accountUID).ID, SLAStatus: model.SLAStatusActive, } require.NoError(t, db.Create(applied).Error) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/download", nil) req.Header.Set("X-Account-ID", aid) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains(t, w.Body.String(), "account_id") }