feat(automation): align automation rule payloads
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/automation"
|
||||
@@ -20,6 +20,29 @@ func NewAutomationRuleHandler(svc *automation.AutomationRuleService) *Automation
|
||||
return &AutomationRuleHandler{svc: svc}
|
||||
}
|
||||
|
||||
type automationRuleRequest struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
EventName string `json:"event_name"`
|
||||
Active *bool `json:"active"`
|
||||
Conditions []automationRuleConditionRequest `json:"conditions"`
|
||||
Actions []automationRuleActionRequest `json:"actions"`
|
||||
}
|
||||
|
||||
type automationRuleConditionRequest struct {
|
||||
AttributeKey string `json:"attribute_key"`
|
||||
Attribute string `json:"attribute"`
|
||||
FilterOperator string `json:"filter_operator"`
|
||||
Values []interface{} `json:"values"`
|
||||
QueryOperator string `json:"query_operator"`
|
||||
CustomAttributeType string `json:"custom_attribute_type"`
|
||||
}
|
||||
|
||||
type automationRuleActionRequest struct {
|
||||
ActionName string `json:"action_name"`
|
||||
ActionParams json.RawMessage `json:"action_params"`
|
||||
}
|
||||
|
||||
// List retrieves all automation rules for an account.
|
||||
// GET /api/v1/accounts/:account_id/automation_rules
|
||||
func (h *AutomationRuleHandler) List(c *gin.Context) {
|
||||
@@ -36,15 +59,15 @@ func (h *AutomationRuleHandler) List(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"automation_rules": rules,
|
||||
"meta": gin.H{"count": len(rules)},
|
||||
"payload": serializeAutomationRules(rules),
|
||||
})
|
||||
}
|
||||
|
||||
// Get retrieves a single automation rule by ID.
|
||||
// GET /api/v1/accounts/:account_id/automation_rules/:id
|
||||
func (h *AutomationRuleHandler) Get(c *gin.Context) {
|
||||
if _, err := parseUintParam(c, "account_id"); err != nil {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -55,13 +78,13 @@ func (h *AutomationRuleHandler) Get(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
rule, svcErr := h.svc.GetByID(c.Request.Context(), automationID)
|
||||
rule, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, rule)
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(rule)})
|
||||
}
|
||||
|
||||
// Create creates a new automation rule.
|
||||
@@ -73,30 +96,35 @@ func (h *AutomationRuleHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var rule automation.AutomationRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
var req automationRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
if rule.Name == "" || rule.EventName == "" {
|
||||
if req.Name == "" || req.EventName == "" {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
|
||||
return
|
||||
}
|
||||
|
||||
rule.AccountID = accountID
|
||||
rule, bindErr := buildAutomationRuleFromRequest(accountID, req)
|
||||
if bindErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, bindErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.Create(c.Request.Context(), &rule); svcErr != nil {
|
||||
if svcErr := h.svc.Create(c.Request.Context(), rule); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, rule)
|
||||
c.JSON(http.StatusOK, serializeAutomationRule(rule))
|
||||
}
|
||||
|
||||
// Update updates an existing automation rule.
|
||||
// PUT /api/v1/accounts/:account_id/automation_rules/:id
|
||||
func (h *AutomationRuleHandler) Update(c *gin.Context) {
|
||||
if _, err := parseUintParam(c, "account_id"); err != nil {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -107,30 +135,41 @@ func (h *AutomationRuleHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
var rule automation.AutomationRule
|
||||
if err := c.ShouldBindJSON(&rule); err != nil {
|
||||
var req automationRuleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrValidation, err.Error())
|
||||
return
|
||||
}
|
||||
if rule.Name == "" || rule.EventName == "" {
|
||||
if req.Name == "" || req.EventName == "" {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, "name and event_name are required")
|
||||
return
|
||||
}
|
||||
|
||||
rule, bindErr := buildAutomationRuleFromRequest(accountID, req)
|
||||
if bindErr != nil {
|
||||
response.AbortWithStatusError(c, http.StatusUnprocessableEntity, response.ErrValidation, bindErr.Error())
|
||||
return
|
||||
}
|
||||
rule.ID = automationID
|
||||
|
||||
if svcErr := h.svc.Update(c.Request.Context(), &rule); svcErr != nil {
|
||||
if svcErr := h.svc.UpdateForAccount(c.Request.Context(), accountID, rule); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.OK(c, rule)
|
||||
updated, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(updated)})
|
||||
}
|
||||
|
||||
// Delete soft-deletes an automation rule.
|
||||
// DELETE /api/v1/accounts/:account_id/automation_rules/:id
|
||||
func (h *AutomationRuleHandler) Delete(c *gin.Context) {
|
||||
if _, err := parseUintParam(c, "account_id"); err != nil {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -141,21 +180,19 @@ func (h *AutomationRuleHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.Delete(c.Request.Context(), automationID); svcErr != nil {
|
||||
if svcErr := h.svc.DeleteForAccount(c.Request.Context(), accountID, automationID); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": strconv.FormatUint(uint64(automationID), 10),
|
||||
"deleted": true,
|
||||
})
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
|
||||
// Clone duplicates an automation rule.
|
||||
// POST /api/v1/accounts/:account_id/automation_rules/:id/clone
|
||||
func (h *AutomationRuleHandler) Clone(c *gin.Context) {
|
||||
if _, err := parseUintParam(c, "account_id"); err != nil {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -166,20 +203,21 @@ func (h *AutomationRuleHandler) Clone(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
cloned, svcErr := h.svc.Clone(c.Request.Context(), automationID)
|
||||
cloned, svcErr := h.svc.CloneForAccount(c.Request.Context(), accountID, automationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
response.Created(c, cloned)
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(cloned)})
|
||||
}
|
||||
|
||||
// ToggleActive toggles the active state of an automation rule.
|
||||
// POST /api/v1/accounts/:account_id/automation_rules/:id/toggle_active
|
||||
// Reference: Chatwoot does not have a toggle_active endpoint; gochat adds this per M6 requirements.
|
||||
func (h *AutomationRuleHandler) ToggleActive(c *gin.Context) {
|
||||
if _, err := parseUintParam(c, "account_id"); err != nil {
|
||||
accountID, err := parseUintParam(c, "account_id")
|
||||
if err != nil {
|
||||
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account_id")
|
||||
return
|
||||
}
|
||||
@@ -202,13 +240,264 @@ func (h *AutomationRuleHandler) ToggleActive(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if svcErr := h.svc.ToggleActive(c.Request.Context(), automationID, *req.Active); svcErr != nil {
|
||||
if svcErr := h.svc.ToggleActiveForAccount(c.Request.Context(), accountID, automationID, *req.Active); svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": strconv.FormatUint(uint64(automationID), 10),
|
||||
"active": *req.Active,
|
||||
})
|
||||
rule, svcErr := h.svc.GetByIDForAccount(c.Request.Context(), accountID, automationID)
|
||||
if svcErr != nil {
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": serializeAutomationRule(rule)})
|
||||
}
|
||||
|
||||
func buildAutomationRuleFromRequest(accountID uint, req automationRuleRequest) (*automation.AutomationRule, error) {
|
||||
active := true
|
||||
if req.Active != nil {
|
||||
active = *req.Active
|
||||
}
|
||||
|
||||
conditions := make(automation.Conditions, 0, len(req.Conditions))
|
||||
for _, condition := range req.Conditions {
|
||||
attribute := condition.Attribute
|
||||
if attribute == "" {
|
||||
attribute = condition.AttributeKey
|
||||
}
|
||||
values := make([]string, 0, len(condition.Values))
|
||||
for _, value := range condition.Values {
|
||||
if value == nil {
|
||||
continue
|
||||
}
|
||||
values = append(values, valueToString(value))
|
||||
}
|
||||
conditions = append(conditions, automation.Condition{
|
||||
Attribute: attribute,
|
||||
AttributeKey: attribute,
|
||||
FilterOperator: automation.NormalizeFilterOperator(condition.FilterOperator),
|
||||
Values: values,
|
||||
QueryOperator: condition.QueryOperator,
|
||||
CustomAttributeType: condition.CustomAttributeType,
|
||||
})
|
||||
}
|
||||
|
||||
actions := make(automation.Actions, 0, len(req.Actions))
|
||||
for _, actionReq := range req.Actions {
|
||||
params, err := normalizeAutomationActionParams(actionReq.ActionName, actionReq.ActionParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
actions = append(actions, automation.Action{ActionName: actionReq.ActionName, ActionParams: params})
|
||||
}
|
||||
|
||||
return &automation.AutomationRule{
|
||||
AccountID: accountID,
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
EventName: req.EventName,
|
||||
Conditions: automation.NormalizeConditions(conditions),
|
||||
Actions: actions,
|
||||
Active: active,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func serializeAutomationRules(rules []automation.AutomationRule) []gin.H {
|
||||
result := make([]gin.H, 0, len(rules))
|
||||
for i := range rules {
|
||||
result = append(result, serializeAutomationRule(&rules[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func serializeAutomationRule(rule *automation.AutomationRule) gin.H {
|
||||
return gin.H{
|
||||
"id": rule.ID,
|
||||
"account_id": rule.AccountID,
|
||||
"name": rule.Name,
|
||||
"description": rule.Description,
|
||||
"event_name": rule.EventName,
|
||||
"conditions": serializeAutomationConditions(rule.Conditions),
|
||||
"actions": serializeAutomationActions(rule.Actions),
|
||||
"created_on": rule.CreatedAt.Unix(),
|
||||
"active": rule.Active,
|
||||
}
|
||||
}
|
||||
|
||||
func serializeAutomationConditions(conditions automation.Conditions) []gin.H {
|
||||
conditions = automation.NormalizeConditions(conditions)
|
||||
result := make([]gin.H, 0, len(conditions))
|
||||
for _, condition := range conditions {
|
||||
item := gin.H{
|
||||
"attribute_key": condition.Attribute,
|
||||
"filter_operator": automation.ChatwootFilterOperator(condition.FilterOperator),
|
||||
"values": condition.Values,
|
||||
}
|
||||
if condition.QueryOperator != "" {
|
||||
item["query_operator"] = condition.QueryOperator
|
||||
}
|
||||
if condition.CustomAttributeType != "" {
|
||||
item["custom_attribute_type"] = condition.CustomAttributeType
|
||||
}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func serializeAutomationActions(actions automation.Actions) []gin.H {
|
||||
result := make([]gin.H, 0, len(actions))
|
||||
for _, action := range actions {
|
||||
result = append(result, gin.H{
|
||||
"action_name": action.ActionName,
|
||||
"action_params": chatwootActionParams(action),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeAutomationActionParams(actionName string, raw json.RawMessage) (map[string]interface{}, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
var object map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &object); err == nil {
|
||||
if object == nil {
|
||||
object = map[string]interface{}{}
|
||||
}
|
||||
return object, nil
|
||||
}
|
||||
var values []interface{}
|
||||
if err := json.Unmarshal(raw, &values); err != nil {
|
||||
var single interface{}
|
||||
if singleErr := json.Unmarshal(raw, &single); singleErr != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = []interface{}{single}
|
||||
}
|
||||
return actionArrayToMap(actionName, values), nil
|
||||
}
|
||||
|
||||
func actionArrayToMap(actionName string, values []interface{}) map[string]interface{} {
|
||||
params := map[string]interface{}{}
|
||||
first := firstValue(values)
|
||||
switch actionName {
|
||||
case "assign_agent":
|
||||
params["assignee_id"] = first
|
||||
case "assign_team", "send_email_to_team":
|
||||
params["team_id"] = first
|
||||
case "add_label", "remove_label":
|
||||
params["labels"] = valuesToStrings(values)
|
||||
case "change_status":
|
||||
params["status"] = first
|
||||
case "change_priority":
|
||||
params["priority"] = first
|
||||
case "send_message", "add_private_note":
|
||||
params["content"] = first
|
||||
case "send_email_to_contact", "send_email_transcript":
|
||||
params["email"] = first
|
||||
case "send_attachment":
|
||||
params["blob_id"] = first
|
||||
case "add_sla":
|
||||
params["sla_policy_id"] = first
|
||||
default:
|
||||
params["values"] = values
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
func chatwootActionParams(action automation.Action) []interface{} {
|
||||
params := action.ActionParams
|
||||
if params == nil {
|
||||
return []interface{}{}
|
||||
}
|
||||
switch action.ActionName {
|
||||
case "assign_agent":
|
||||
return compactValues(params["assignee_id"], params["agent_id"])
|
||||
case "assign_team", "send_email_to_team":
|
||||
return compactValues(params["team_id"])
|
||||
case "add_label", "remove_label":
|
||||
if labels, ok := params["labels"]; ok {
|
||||
return valuesToInterfaces(labels)
|
||||
}
|
||||
return compactValues(params["label"])
|
||||
case "change_status":
|
||||
return compactValues(params["status"])
|
||||
case "change_priority":
|
||||
return compactValues(params["priority"])
|
||||
case "send_message", "add_private_note":
|
||||
return compactValues(params["content"], params["message"])
|
||||
case "send_email_to_contact", "send_email_transcript":
|
||||
return compactValues(params["email"])
|
||||
case "send_attachment":
|
||||
return compactValues(params["blob_id"], params["attachment_url"])
|
||||
case "add_sla":
|
||||
return compactValues(params["sla_policy_id"])
|
||||
default:
|
||||
if values, ok := params["values"]; ok {
|
||||
return valuesToInterfaces(values)
|
||||
}
|
||||
return []interface{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func firstValue(values []interface{}) interface{} {
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return values[0]
|
||||
}
|
||||
|
||||
func compactValues(values ...interface{}) []interface{} {
|
||||
for _, value := range values {
|
||||
if value != nil {
|
||||
return []interface{}{value}
|
||||
}
|
||||
}
|
||||
return []interface{}{}
|
||||
}
|
||||
|
||||
func valuesToInterfaces(value interface{}) []interface{} {
|
||||
switch typed := value.(type) {
|
||||
case []interface{}:
|
||||
return typed
|
||||
case []string:
|
||||
result := make([]interface{}, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
case nil:
|
||||
return []interface{}{}
|
||||
default:
|
||||
return []interface{}{typed}
|
||||
}
|
||||
}
|
||||
|
||||
func valuesToStrings(values []interface{}) []string {
|
||||
result := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if value != nil {
|
||||
result = append(result, valueToString(value))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func valueToString(value interface{}) string {
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
return typed
|
||||
case json.Number:
|
||||
return typed.String()
|
||||
default:
|
||||
return jsonNumberSafeString(typed)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonNumberSafeString(value interface{}) string {
|
||||
bytes, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func (s *AutomationRuleHandlerTestSuite) SetupSuite() {
|
||||
r := gin.New()
|
||||
s.router = r
|
||||
|
||||
accountGroup := r.Group("/api/v1/accounts/:account_id")
|
||||
accountGroup := r.Group("/api/v1/accounts/:account_id")
|
||||
{
|
||||
rulesGroup := accountGroup.Group("/automation_rules")
|
||||
{
|
||||
@@ -106,8 +106,11 @@ func (s *AutomationRuleHandlerTestSuite) TestList_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
rules := resp["automation_rules"].([]interface{})
|
||||
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() {
|
||||
@@ -118,7 +121,7 @@ func (s *AutomationRuleHandlerTestSuite) TestList_Empty() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
rules := resp["automation_rules"].([]interface{})
|
||||
rules := resp["payload"].([]interface{})
|
||||
s.Equal(0, len(rules))
|
||||
}
|
||||
|
||||
@@ -137,6 +140,12 @@ func (s *AutomationRuleHandlerTestSuite) TestGet_Success() {
|
||||
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() {
|
||||
@@ -155,18 +164,32 @@ func (s *AutomationRuleHandlerTestSuite) TestGet_NotFound() {
|
||||
|
||||
// --- Create tests ---
|
||||
func (s *AutomationRuleHandlerTestSuite) TestCreate_Success() {
|
||||
body := `{"event_name":"conversation_created","name":"New Rule","active":true,"conditions":[{"attribute":"status","filter_operator":"equal","values":["open"],"query_operator":"and"}],"actions":[{"action_name":"assign_team","action_params":{"team_id":1}}]}`
|
||||
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.StatusCreated, w.Code)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
s.Equal("New Rule", data["name"])
|
||||
s.Equal(float64(1), data["account_id"])
|
||||
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() {
|
||||
@@ -190,12 +213,25 @@ func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidAccountID() {
|
||||
func (s *AutomationRuleHandlerTestSuite) TestUpdate_Success() {
|
||||
rule := s.createRule(1, "conversation_created", "OldName", true)
|
||||
|
||||
body := fmt.Sprintf(`{"event_name":"message_created","name":"UpdatedName","active":false,"conditions":[{"attribute":"status","filter_operator":"equal","values":["resolved"],"query_operator":"and"}],"actions":[{"action_name":"send_message","action_params":{"message":"Hello"}}]}`)
|
||||
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() {
|
||||
@@ -216,13 +252,12 @@ func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidJSON() {
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestUpdate_NotFound() {
|
||||
body := `{"name":"Updated","active":true}`
|
||||
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)
|
||||
// Service wraps "record not found" error → handleServiceError returns 500
|
||||
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
||||
s.Equal(http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- Delete tests ---
|
||||
@@ -233,10 +268,7 @@ func (s *AutomationRuleHandlerTestSuite) TestDelete_Success() {
|
||||
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)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["deleted"])
|
||||
s.Empty(w.Body.String())
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestDelete_InvalidID() {
|
||||
@@ -247,11 +279,10 @@ func (s *AutomationRuleHandlerTestSuite) TestDelete_InvalidID() {
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestDelete_NotFound() {
|
||||
// GORM Delete on non-existent ID returns nil error — handler returns 200
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/accounts/1/automation_rules/9999", nil)
|
||||
s.router.ServeHTTP(w, req)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Equal(http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
// --- Clone tests ---
|
||||
@@ -261,11 +292,11 @@ func (s *AutomationRuleHandlerTestSuite) TestClone_Success() {
|
||||
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.StatusCreated, w.Code)
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
data := resp["data"].(map[string]interface{})
|
||||
data := resp["payload"].(map[string]interface{})
|
||||
s.Equal("OriginalRule (copy)", data["name"])
|
||||
s.Equal(false, data["active"])
|
||||
}
|
||||
@@ -297,7 +328,7 @@ func (s *AutomationRuleHandlerTestSuite) TestToggleActive_Success() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(true, resp["active"])
|
||||
s.Equal(true, resp["payload"].(map[string]interface{})["active"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_SetInactive() {
|
||||
@@ -312,7 +343,7 @@ func (s *AutomationRuleHandlerTestSuite) TestToggleActive_SetInactive() {
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal(false, resp["active"])
|
||||
s.Equal(false, resp["payload"].(map[string]interface{})["active"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestToggleActive_InvalidID() {
|
||||
@@ -338,10 +369,9 @@ func (s *AutomationRuleHandlerTestSuite) TestToggleActive_NotFound() {
|
||||
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)
|
||||
// ToggleActive updates via GORM Updates — no error for non-existent, just 0 rows affected
|
||||
s.Equal(http.StatusOK, w.Code)
|
||||
s.Equal(http.StatusNotFound, w.Code)
|
||||
}
|
||||
|
||||
func TestAutomationRuleHandlerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(AutomationRuleHandlerTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user