Files
gochat/internal/handler/api/v1/automation_rule_handler_test.go
T

378 lines
14 KiB
Go

package v1
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/automation"
)
// automationDBProvider wraps *gorm.DB to implement automation.DBProvider.
type automationDBProvider struct {
db *gorm.DB
}
func (p *automationDBProvider) DB() *gorm.DB { return p.db }
// AutomationRuleHandlerTestSuite tests AutomationRuleHandler with real SQLite DB.
type AutomationRuleHandlerTestSuite struct {
suite.Suite
db *gorm.DB
router *gin.Engine
handler *AutomationRuleHandler
}
func (s *AutomationRuleHandlerTestSuite) SetupSuite() {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open("file::memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
s.Require().NoError(err, "failed to open SQLite test database")
s.Require().NoError(db.AutoMigrate(&automation.AutomationRule{}), "failed to auto-migrate AutomationRule model")
s.db = db
provider := &automationDBProvider{db: db}
svc := automation.NewAutomationRuleService(provider)
s.handler = NewAutomationRuleHandler(svc)
r := gin.New()
s.router = r
accountGroup := r.Group("/api/v1/accounts/:account_id")
{
rulesGroup := accountGroup.Group("/automation_rules")
{
rulesGroup.GET("", s.handler.List)
rulesGroup.GET("/:automation_id", s.handler.Get)
rulesGroup.POST("", s.handler.Create)
rulesGroup.PUT("/:automation_id", s.handler.Update)
rulesGroup.DELETE("/:automation_id", s.handler.Delete)
rulesGroup.POST("/:automation_id/clone", s.handler.Clone)
rulesGroup.POST("/:automation_id/toggle_active", s.handler.ToggleActive)
}
}
}
func (s *AutomationRuleHandlerTestSuite) TearDownSuite() {
sqlDB, err := s.db.DB()
if err == nil {
sqlDB.Close()
}
}
func (s *AutomationRuleHandlerTestSuite) SetupTest() {
s.db.Exec("DELETE FROM automation_rules")
}
func (s *AutomationRuleHandlerTestSuite) createRule(accountID uint, eventName, name string, active bool) *automation.AutomationRule {
rule := &automation.AutomationRule{
AccountID: accountID,
EventName: eventName,
Name: name,
Active: active,
Conditions: automation.Conditions{
{Attribute: "status", FilterOperator: "equal", Values: []string{"open"}, QueryOperator: "and"},
},
Actions: automation.Actions{
{ActionName: "assign_team", ActionParams: map[string]interface{}{"team_id": float64(1)}},
},
}
err := s.db.Create(rule).Error
s.Require().NoError(err)
return rule
}
// --- List tests ---
func (s *AutomationRuleHandlerTestSuite) TestList_Success() {
s.createRule(1, "conversation_created", "Rule1", true)
s.createRule(1, "message_created", "Rule2", false)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
rules := resp["payload"].([]interface{})
s.Equal(2, len(rules))
rule := rules[0].(map[string]interface{})
s.Equal("status", rule["conditions"].([]interface{})[0].(map[string]interface{})["attribute_key"])
s.Equal("equal_to", rule["conditions"].([]interface{})[0].(map[string]interface{})["filter_operator"])
}
func (s *AutomationRuleHandlerTestSuite) TestList_Empty() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
rules := resp["payload"].([]interface{})
s.Equal(0, len(rules))
}
func (s *AutomationRuleHandlerTestSuite) TestList_InvalidAccountID() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/abc/automation_rules", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
// --- Get tests ---
func (s *AutomationRuleHandlerTestSuite) TestGet_Success() {
rule := s.createRule(1, "conversation_created", "TestRule", true)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].(map[string]interface{})
s.Equal("TestRule", payload["name"])
s.Equal(float64(1), payload["account_id"])
}
func (s *AutomationRuleHandlerTestSuite) TestGet_InvalidID() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules/abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestGet_NotFound() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/automation_rules/9999", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// --- Create tests ---
func (s *AutomationRuleHandlerTestSuite) TestCreate_Success() {
body := `{"event_name":"conversation_created","name":"New Rule","active":true,"conditions":[{"attribute_key":"status","filter_operator":"equal_to","values":["open"],"query_operator":"and"}],"actions":[{"action_name":"assign_team","action_params":[1]}]}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal("New Rule", resp["name"])
s.Equal(float64(1), resp["account_id"])
s.NotContains(resp, "data")
s.NotContains(resp, "success")
condition := resp["conditions"].([]interface{})[0].(map[string]interface{})
s.Equal("status", condition["attribute_key"])
s.Equal("equal_to", condition["filter_operator"])
s.NotContains(condition, "attribute")
action := resp["actions"].([]interface{})[0].(map[string]interface{})
s.Equal([]interface{}{float64(1)}, action["action_params"])
var saved automation.AutomationRule
s.NoError(s.db.First(&saved, uint(resp["id"].(float64))).Error)
s.Equal(uint(1), saved.AccountID)
s.Equal("status", saved.Conditions[0].Attribute)
s.Equal("equal", saved.Conditions[0].FilterOperator)
s.Equal(float64(1), saved.Actions[0].ActionParams["team_id"])
}
func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidJSON() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(`{invalid`))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidAccountID() {
body := `{"event_name":"conversation_created","name":"Rule","active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/abc/automation_rules", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
// --- Update tests ---
func (s *AutomationRuleHandlerTestSuite) TestUpdate_Success() {
rule := s.createRule(1, "conversation_created", "OldName", true)
body := `{"event_name":"message_created","name":"UpdatedName","active":false,"conditions":[{"attribute_key":"status","filter_operator":"equal_to","values":["resolved"],"query_operator":"and"}],"actions":[{"action_name":"send_message","action_params":["Hello"]}]}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.ID), bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
payload := resp["payload"].(map[string]interface{})
s.Equal("UpdatedName", payload["name"])
s.Equal(float64(1), payload["account_id"])
s.Equal(false, payload["active"])
s.Equal([]interface{}{"Hello"}, payload["actions"].([]interface{})[0].(map[string]interface{})["action_params"])
var saved automation.AutomationRule
s.NoError(s.db.First(&saved, rule.ID).Error)
s.Equal(uint(1), saved.AccountID)
s.Equal("UpdatedName", saved.Name)
}
func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidID() {
body := `{"name":"Updated","active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/abc", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidJSON() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/1", bytes.NewBufferString(`{invalid`))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestUpdate_NotFound() {
body := `{"name":"Updated","event_name":"message_created","conditions":[],"actions":[],"active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPut, "/api/v1/accounts/1/automation_rules/9999", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// --- Delete tests ---
func (s *AutomationRuleHandlerTestSuite) TestDelete_Success() {
rule := s.createRule(1, "conversation_created", "ToDelete", true)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", rule.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
s.Empty(w.Body.String())
}
func (s *AutomationRuleHandlerTestSuite) TestDelete_InvalidID() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/accounts/1/automation_rules/abc", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestDelete_NotFound() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodDelete, "/api/v1/accounts/1/automation_rules/9999", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// --- Clone tests ---
func (s *AutomationRuleHandlerTestSuite) TestClone_Success() {
rule := s.createRule(1, "conversation_created", "OriginalRule", true)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/clone", rule.ID), nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
data := resp["payload"].(map[string]interface{})
s.Equal("OriginalRule (copy)", data["name"])
s.Equal(false, data["active"])
}
func (s *AutomationRuleHandlerTestSuite) TestClone_InvalidID() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/abc/clone", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestClone_NotFound() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/9999/clone", nil)
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
// --- ToggleActive tests ---
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_Success() {
rule := s.createRule(1, "conversation_created", "ToggleRule", false)
body := `{"active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/toggle_active", rule.ID), bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(true, resp["payload"].(map[string]interface{})["active"])
}
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_SetInactive() {
rule := s.createRule(1, "conversation_created", "ActiveRule", true)
body := `{"active":false}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/toggle_active", rule.ID), bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusOK, w.Code)
var resp map[string]interface{}
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
s.Equal(false, resp["payload"].(map[string]interface{})["active"])
}
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_InvalidID() {
body := `{"active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/abc/toggle_active", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_InvalidJSON() {
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/1/toggle_active", bytes.NewBufferString(`{invalid`))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusBadRequest, w.Code)
}
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_NotFound() {
body := `{"active":true}`
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules/9999/toggle_active", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
s.router.ServeHTTP(w, req)
s.Equal(http.StatusNotFound, w.Code)
}
func TestAutomationRuleHandlerTestSuite(t *testing.T) {
suite.Run(t, new(AutomationRuleHandlerTestSuite))
}