feat(automation): align automation rule payloads

This commit is contained in:
2026-06-05 09:03:34 +08:00
parent 04be5e4e4e
commit 3403770cb7
7 changed files with 612 additions and 118 deletions
@@ -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))
}
}