* H-300: wire Captain Skills into Web runtime * H-300: enforce effective model and conservative skill budget * H-300: fix CI gosec step * ci: extend golangci-lint timeout * fix lint findings across backend * fix(push): resolve delivery protocol blockers * test(repository): close SQLite test databases * test(repository): reuse SQLite schema per package * H-307: restore backend Go cache in CI * H-307: prefetch modules before cold lint * H-307: resolve govulncheck security gate * H-307: build lint with patched Go toolchain * H-307: clear remaining security scan findings --------- Co-authored-by: Rogee <rogee@ipao.vip>
788 lines
29 KiB
Go
788 lines
29 KiB
Go
package v1
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"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{},
|
|
&model.Inbox{},
|
|
&model.Contact{},
|
|
&model.Conversation{},
|
|
&model.User{},
|
|
&model.Team{},
|
|
&model.Audit{},
|
|
))
|
|
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)
|
|
auditSvc := service.NewAuditService(repository.NewAuditRepo(db))
|
|
handler := NewSlaPolicyHandler(svc).WithAuditService(auditSvc)
|
|
|
|
// 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 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", handler.ListAppliedSlas)
|
|
rg.GET("/applied_slas/metrics", handler.GetAppliedSlaMetrics)
|
|
rg.GET("/applied_slas/download", handler.GetAppliedSlaDownload)
|
|
return r
|
|
}
|
|
|
|
func setupSlaPolicyAuditRouter(handler *SlaPolicyHandler, accountID uint, userID uint) *gin.Engine {
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
r.Use(func(c *gin.Context) {
|
|
c.Set("account_id", accountID)
|
|
c.Set("user_id", userID)
|
|
c.Next()
|
|
})
|
|
rg := r.Group("/api/v1/accounts/:account_id")
|
|
rg.POST("/sla_policies", handler.Create)
|
|
rg.PUT("/sla_policies/:id", handler.Update)
|
|
rg.DELETE("/sla_policies/:id", handler.Delete)
|
|
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)
|
|
_, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{
|
|
Name: "Policy-A", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
_, err = svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{
|
|
Name: "Policy-B", FirstResponseTimeThreshold: 20, NextResponseTimeThreshold: 40, ResolutionTimeThreshold: 200,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.NotContains(t, got, "success")
|
|
require.IsType(t, []interface{}{}, got["payload"])
|
|
payload := got["payload"].([]interface{})
|
|
require.Len(t, payload, 2)
|
|
|
|
first := payload[0].(map[string]interface{})
|
|
assert.Equal(t, "Policy-A", first["name"])
|
|
assert.Equal(t, float64(10), first["first_response_time_threshold"])
|
|
assert.NotContains(t, first, "created_at")
|
|
assert.NotContains(t, first, "account_id")
|
|
}
|
|
|
|
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.StatusOK, w.Code)
|
|
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.NotContains(t, got, "success")
|
|
payload := got["payload"].(map[string]interface{})
|
|
assert.Equal(t, "New SLA", payload["name"])
|
|
assert.Equal(t, "desc", payload["description"])
|
|
assert.Equal(t, float64(30), payload["first_response_time_threshold"])
|
|
assert.NotContains(t, payload, "created_at")
|
|
assert.NotContains(t, payload, "account_id")
|
|
}
|
|
|
|
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_MutationsWriteAuditEntries(t *testing.T) {
|
|
handler, db := setupSlaPolicyHandlerTest(t)
|
|
accountID := slaHandlerAccountIDUint(db)
|
|
router := setupSlaPolicyAuditRouter(handler, accountID, 88)
|
|
|
|
createBody, _ := json.Marshal(map[string]any{"sla_policy": map[string]any{
|
|
"name": "Audit SLA",
|
|
"first_response_time_threshold": 15,
|
|
"next_response_time_threshold": 30,
|
|
"resolution_time_threshold": 90,
|
|
}})
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies", bytes.NewBuffer(createBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Request-ID", "sla-audit-create")
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
var created map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &created))
|
|
policyID := uint(created["payload"].(map[string]any)["id"].(float64))
|
|
|
|
updateBody, _ := json.Marshal(map[string]any{"sla_policy": map[string]any{
|
|
"name": "Audit SLA Updated",
|
|
"first_response_time_threshold": 20,
|
|
"next_response_time_threshold": 40,
|
|
"resolution_time_threshold": 100,
|
|
}})
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest(http.MethodPut, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies/"+strconv.FormatUint(uint64(policyID), 10), bytes.NewBuffer(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/sla_policies/"+strconv.FormatUint(uint64(policyID), 10), nil)
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
|
|
var audits []model.Audit
|
|
require.NoError(t, db.Order("id ASC").Find(&audits).Error)
|
|
require.Len(t, audits, 3)
|
|
for _, audit := range audits {
|
|
assert.Equal(t, accountID, *audit.AccountID)
|
|
assert.Equal(t, "Account", audit.AssociatedType)
|
|
assert.Equal(t, accountID, *audit.AssociatedID)
|
|
assert.Equal(t, uint(88), *audit.UserID)
|
|
assert.Equal(t, "SlaPolicy", audit.AuditableType)
|
|
assert.Equal(t, policyID, audit.AuditableID)
|
|
assert.NotEmpty(t, audit.AuditedChanges)
|
|
}
|
|
assert.Equal(t, "create", audits[0].Action)
|
|
assert.Equal(t, "sla-audit-create", audits[0].RequestUUID)
|
|
assert.Equal(t, "update", audits[1].Action)
|
|
assert.Equal(t, "destroy", audits[2].Action)
|
|
}
|
|
|
|
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, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{
|
|
Name: "GetTest", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.NotContains(t, got, "success")
|
|
payload := got["payload"].(map[string]interface{})
|
|
assert.Equal(t, "GetTest", payload["name"])
|
|
assert.Equal(t, float64(10), payload["first_response_time_threshold"])
|
|
assert.NotContains(t, payload, "created_at")
|
|
assert.NotContains(t, payload, "account_id")
|
|
}
|
|
|
|
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, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{
|
|
Name: "ToUpdate", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.NotContains(t, got, "success")
|
|
payload := got["payload"].(map[string]interface{})
|
|
assert.Equal(t, "Updated Name", payload["name"])
|
|
assert.Equal(t, float64(45), payload["first_response_time_threshold"])
|
|
}
|
|
|
|
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, err := svc.Create(context.Background(), slaHandlerAccountIDUint(db), &service.CreateSlaPolicyRequest{
|
|
Name: "ToDelete", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
assert.Empty(t, w.Body.String())
|
|
}
|
|
|
|
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, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{
|
|
Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
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, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{
|
|
Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
_, err = svc.AddInbox(context.Background(), accountUID, policy.ID, inbox.ID)
|
|
require.NoError(t, err)
|
|
|
|
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, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{
|
|
Name: "Test SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
inbox := &model.Inbox{Name: "Inbox1", AccountID: accountUID}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
_, err = svc.AddInbox(context.Background(), accountUID, policy.ID, inbox.ID)
|
|
require.NoError(t, err)
|
|
|
|
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)
|
|
}
|
|
|
|
func TestSlaPolicyHandler_InboxAssociationChatwootPayloadAndSideEffects(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, err := svc.Create(context.Background(), accountUID, &service.CreateSlaPolicyRequest{
|
|
Name: "Inbox Linked SLA", FirstResponseTimeThreshold: 10, NextResponseTimeThreshold: 20, ResolutionTimeThreshold: 100,
|
|
})
|
|
require.NoError(t, err)
|
|
inbox := &model.Inbox{Name: "Priority Inbox", AccountID: accountUID, ChannelType: "web_widget"}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
|
|
addBody, _ := json.Marshal(map[string]any{"inbox_id": inbox.ID})
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", bytes.NewBuffer(addBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusCreated, w.Code, w.Body.String())
|
|
|
|
var addResp map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &addResp))
|
|
payload := addResp["data"].(map[string]any)
|
|
assert.Equal(t, float64(policy.ID), payload["sla_policy_id"])
|
|
assert.Equal(t, float64(inbox.ID), payload["inbox_id"])
|
|
assert.Equal(t, float64(accountUID), payload["account_id"])
|
|
assert.Equal(t, true, addResp["success"])
|
|
|
|
var associationCount int64
|
|
require.NoError(t, db.Model(&model.SlaPolicyInbox{}).Where("sla_policy_id = ? AND inbox_id = ? AND account_id = ?", policy.ID, inbox.ID, accountUID).Count(&associationCount).Error)
|
|
assert.Equal(t, int64(1), associationCount)
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest(http.MethodGet, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes", nil)
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
|
|
|
|
var listResp map[string]any
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &listResp))
|
|
listPayload := listResp["data"].([]any)
|
|
require.Len(t, listPayload, 1)
|
|
listed := listPayload[0].(map[string]any)
|
|
assert.Equal(t, float64(policy.ID), listed["sla_policy_id"])
|
|
assert.Equal(t, float64(inbox.ID), listed["inbox_id"])
|
|
assert.Equal(t, float64(accountUID), listed["account_id"])
|
|
|
|
w = httptest.NewRecorder()
|
|
req, _ = http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+aid+"/sla_policies/"+strconv.FormatUint(uint64(policy.ID), 10)+"/inboxes/"+strconv.FormatUint(uint64(inbox.ID), 10), nil)
|
|
router.ServeHTTP(w, req)
|
|
require.Equal(t, http.StatusNoContent, w.Code, w.Body.String())
|
|
|
|
require.NoError(t, db.Model(&model.SlaPolicyInbox{}).Where("sla_policy_id = ? AND inbox_id = ? AND account_id = ?", policy.ID, inbox.ID, accountUID).Count(&associationCount).Error)
|
|
assert.Equal(t, int64(0), associationCount)
|
|
}
|
|
|
|
// ========== Applied SLA reports ==========
|
|
|
|
func TestSlaPolicyHandler_ListAppliedSlas_ChatwootPayloadAndFilters(t *testing.T) {
|
|
handler, db := setupSlaPolicyHandlerTest(t)
|
|
router := setupSlaPolicyTestRouter(handler)
|
|
aid := slaHandlerAccountID(db)
|
|
accountUID := slaHandlerAccountIDUint(db)
|
|
now := time.Now().UTC()
|
|
|
|
policy, conversation, assignee := seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusActiveWithMisses, "vip, urgent", now.Add(-time.Hour))
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour))
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "other", now.Add(-time.Hour))
|
|
|
|
w := httptest.NewRecorder()
|
|
url := "/api/v1/accounts/" + aid + "/applied_slas?" +
|
|
"since=" + strconv.FormatInt(now.Add(-2*time.Hour).Unix(), 10) +
|
|
"&until=" + strconv.FormatInt(now.Add(time.Hour).Unix(), 10) +
|
|
"&inbox_id=" + strconv.FormatUint(uint64(conversation.InboxID), 10) +
|
|
"&team_id=" + strconv.FormatUint(uint64(*conversation.TeamID), 10) +
|
|
"&assigned_agent_id=" + strconv.FormatUint(uint64(assignee.ID), 10) +
|
|
"&sla_policy_id=" + strconv.FormatUint(uint64(policy.ID), 10) +
|
|
"&label_list=urgent&page=1"
|
|
req, _ := http.NewRequest("GET", url, nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.NotContains(t, got, "success")
|
|
|
|
payload := got["payload"].([]interface{})
|
|
require.Len(t, payload, 1)
|
|
item := payload[0].(map[string]interface{})
|
|
applied := item["applied_sla"].(map[string]interface{})
|
|
assert.Equal(t, "Gold SLA", applied["sla_name"])
|
|
assert.Equal(t, "active_with_misses", applied["sla_status"])
|
|
assert.Equal(t, float64(10), applied["sla_first_response_time_threshold"])
|
|
|
|
conversationPayload := item["conversation"].(map[string]interface{})
|
|
assert.Equal(t, float64(*conversation.DisplayID), conversationPayload["id"])
|
|
assert.Equal(t, "vip, urgent", conversationPayload["labels"])
|
|
contact := conversationPayload["contact"].(map[string]interface{})
|
|
assert.Equal(t, "SLA Contact", contact["name"])
|
|
assigneePayload := conversationPayload["assignee"].(map[string]interface{})
|
|
assert.Equal(t, "Agent One", assigneePayload["name"])
|
|
|
|
events := item["sla_events"].([]interface{})
|
|
require.Len(t, events, 1)
|
|
assert.Equal(t, "nrt", events[0].(map[string]interface{})["event_type"])
|
|
|
|
meta := got["meta"].(map[string]interface{})
|
|
assert.Equal(t, float64(1), meta["count"])
|
|
assert.Equal(t, float64(1), meta["current_page"])
|
|
}
|
|
|
|
func TestSlaPolicyHandler_GetAppliedSlaMetrics_ChatwootReportShape(t *testing.T) {
|
|
handler, db := setupSlaPolicyHandlerTest(t)
|
|
router := setupSlaPolicyTestRouter(handler)
|
|
aid := slaHandlerAccountID(db)
|
|
accountUID := slaHandlerAccountIDUint(db)
|
|
now := time.Now().UTC()
|
|
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour))
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "vip", now.Add(-time.Hour))
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusActiveWithMisses, "other", now.Add(-time.Hour))
|
|
|
|
w := httptest.NewRecorder()
|
|
req, _ := http.NewRequest("GET", "/api/v1/accounts/"+aid+"/applied_slas/metrics?label_list=vip", nil)
|
|
router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
var got map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &got))
|
|
assert.Equal(t, float64(2), got["total_applied_slas"])
|
|
assert.Equal(t, float64(1), got["number_of_sla_misses"])
|
|
assert.Equal(t, "50.0%", got["hit_rate"])
|
|
}
|
|
|
|
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)
|
|
now := time.Now().UTC()
|
|
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusMissed, "vip", now.Add(-time.Hour))
|
|
seedAppliedSlaReportRecord(t, db, accountUID, model.SLAStatusHit, "vip", now.Add(-time.Hour))
|
|
|
|
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.Equal(t, "attachment; filename=breached_conversation.csv", w.Header().Get("Content-Disposition"))
|
|
assert.Contains(t, w.Body.String(), "Conversation ID,SLA policy breached,Assignee,Team,Inbox,Labels,Conversation link,Breached events")
|
|
assert.Contains(t, w.Body.String(), "Gold SLA")
|
|
assert.Contains(t, w.Body.String(), "frt")
|
|
assert.NotContains(t, w.Body.String(), "hit")
|
|
}
|
|
|
|
func seedAppliedSlaReportRecord(t *testing.T, db *gorm.DB, accountID uint, status model.SLAStatus, labels string, createdAt time.Time) (*model.SlaPolicy, *model.Conversation, *model.User) {
|
|
t.Helper()
|
|
policy := &model.SlaPolicy{
|
|
AccountID: accountID,
|
|
Name: "Gold SLA",
|
|
Description: "Gold support",
|
|
FirstResponseTimeThreshold: 10,
|
|
NextResponseTimeThreshold: 20,
|
|
ResolutionTimeThreshold: 30,
|
|
}
|
|
require.NoError(t, db.Create(policy).Error)
|
|
|
|
inbox := &model.Inbox{AccountID: accountID, Name: "Priority", ChannelType: "web_widget", ChannelID: 1}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
contact := &model.Contact{AccountID: accountID, Name: "SLA Contact"}
|
|
require.NoError(t, db.Create(contact).Error)
|
|
assignee := &model.User{AccountID: accountID, Name: "Agent One", Email: "agent-" + strconv.FormatInt(time.Now().UnixNano(), 10) + "@example.com", Password: "secret", Role: "agent"}
|
|
require.NoError(t, db.Create(assignee).Error)
|
|
team := &model.Team{AccountID: accountID, Name: "Escalation"}
|
|
require.NoError(t, db.Create(team).Error)
|
|
displayID := uint(100 + time.Now().UnixNano()%100000)
|
|
conversation := &model.Conversation{
|
|
AccountID: accountID,
|
|
InboxID: inbox.ID,
|
|
ContactID: contact.ID,
|
|
AssigneeID: &assignee.ID,
|
|
TeamID: &team.ID,
|
|
DisplayID: &displayID,
|
|
Status: "open",
|
|
ChannelType: "Channel::WebWidget",
|
|
Channel: "web_widget",
|
|
Labels: labels,
|
|
}
|
|
require.NoError(t, db.Create(conversation).Error)
|
|
|
|
applied := &model.AppliedSLA{AccountID: accountID, ConversationID: conversation.ID, SlaPolicyID: policy.ID, SLAStatus: status}
|
|
require.NoError(t, db.Create(applied).Error)
|
|
require.NoError(t, db.Model(applied).Updates(map[string]any{"created_at": createdAt, "updated_at": createdAt}).Error)
|
|
applied.CreatedAt = createdAt
|
|
applied.UpdatedAt = createdAt
|
|
|
|
if status == model.SLAStatusMissed || status == model.SLAStatusActiveWithMisses {
|
|
eventType := model.SLAEventFRT
|
|
if status == model.SLAStatusActiveWithMisses {
|
|
eventType = model.SLAEventNRT
|
|
}
|
|
require.NoError(t, db.Create(&model.SlaEvent{
|
|
AppliedSlaID: applied.ID,
|
|
AccountID: accountID,
|
|
ConversationID: conversation.ID,
|
|
InboxID: inbox.ID,
|
|
SlaPolicyID: policy.ID,
|
|
EventType: eventType,
|
|
}).Error)
|
|
}
|
|
|
|
return policy, conversation, assignee
|
|
}
|