feat(automation): align attachment uploads
This commit is contained in:
@@ -84,19 +84,30 @@ func (a *Actions) Scan(value interface{}) error {
|
||||
// Reference: Chatwoot AutomationRule — account_id, event_name, conditions, actions, active, name, description
|
||||
type AutomationRule struct {
|
||||
model.Base
|
||||
AccountID uint `gorm:"index;not null" json:"account_id"`
|
||||
EventName string `gorm:"size:100;index;not null" json:"event_name"` // e.g. "conversation_created", "message_created"
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description,omitempty"`
|
||||
Conditions Conditions `gorm:"type:jsonb;default:'[]'" json:"conditions"`
|
||||
Actions Actions `gorm:"type:jsonb;default:'[]'" json:"actions"`
|
||||
Active bool `gorm:"not null" json:"active"`
|
||||
ActiveAt *time.Time `gorm:"index" json:"active_at,omitempty"`
|
||||
InactiveAt *time.Time `gorm:"index" json:"inactive_at,omitempty"`
|
||||
AccountID uint `gorm:"index;not null" json:"account_id"`
|
||||
EventName string `gorm:"size:100;index;not null" json:"event_name"` // e.g. "conversation_created", "message_created"
|
||||
Name string `gorm:"size:255;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description,omitempty"`
|
||||
Conditions Conditions `gorm:"type:jsonb;default:'[]'" json:"conditions"`
|
||||
Actions Actions `gorm:"type:jsonb;default:'[]'" json:"actions"`
|
||||
Active bool `gorm:"not null" json:"active"`
|
||||
ActiveAt *time.Time `gorm:"index" json:"active_at,omitempty"`
|
||||
InactiveAt *time.Time `gorm:"index" json:"inactive_at,omitempty"`
|
||||
Files []AutomationRuleFile `gorm:"-" json:"files,omitempty"`
|
||||
}
|
||||
|
||||
func (AutomationRule) TableName() string { return "automation_rules" }
|
||||
|
||||
type AutomationRuleFile struct {
|
||||
ID uint `json:"id"`
|
||||
AutomationRuleID uint `json:"automation_rule_id"`
|
||||
FileType string `json:"file_type"`
|
||||
AccountID uint `json:"account_id"`
|
||||
FileURL string `json:"file_url"`
|
||||
BlobID uint `json:"blob_id"`
|
||||
Filename string `json:"filename"`
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// Macro model
|
||||
// ===========================
|
||||
|
||||
@@ -2,9 +2,12 @@ package automation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
"github.com/gochat/gochat/internal/worker"
|
||||
applogger "github.com/gochat/gochat/pkg/logger"
|
||||
"gorm.io/gorm"
|
||||
@@ -44,6 +47,7 @@ func (s *AutomationRuleService) GetByID(ctx context.Context, id uint) (*Automati
|
||||
if err := s.db.DB().WithContext(ctx).First(&rule, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.hydrateAutomationRuleFiles(ctx, &rule)
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
@@ -55,6 +59,7 @@ func (s *AutomationRuleService) GetByIDForAccount(ctx context.Context, accountID
|
||||
First(&rule, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_ = s.hydrateAutomationRuleFiles(ctx, &rule)
|
||||
return &rule, nil
|
||||
}
|
||||
|
||||
@@ -67,6 +72,9 @@ func (s *AutomationRuleService) ListByAccount(ctx context.Context, accountID uin
|
||||
Find(&rules).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range rules {
|
||||
_ = s.hydrateAutomationRuleFiles(ctx, &rules[i])
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
@@ -91,16 +99,22 @@ func (s *AutomationRuleService) Create(ctx context.Context, rule *AutomationRule
|
||||
if err := ValidateConditions(rule.Conditions); err != nil {
|
||||
return fmt.Errorf("invalid conditions: %w", err)
|
||||
}
|
||||
if err := s.normalizeAutomationRuleAttachmentActions(ctx, rule.AccountID, &rule.Actions); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate actions before saving
|
||||
if err := ValidateActions(rule.Actions); err != nil {
|
||||
return fmt.Errorf("invalid actions: %w", err)
|
||||
}
|
||||
// Use Select to force all fields including zero-value bool Active=false
|
||||
// Without Select, GORM skips zero-value fields and uses column defaults.
|
||||
return s.db.DB().WithContext(ctx).Select(
|
||||
if err := s.db.DB().WithContext(ctx).Select(
|
||||
"AccountID", "EventName", "Name", "Description",
|
||||
"Conditions", "Actions", "Active",
|
||||
).Create(rule).Error
|
||||
).Create(rule).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return s.hydrateAutomationRuleFiles(ctx, rule)
|
||||
}
|
||||
|
||||
// Update updates an existing automation rule.
|
||||
@@ -110,6 +124,9 @@ func (s *AutomationRuleService) Update(ctx context.Context, rule *AutomationRule
|
||||
if err := ValidateConditions(rule.Conditions); err != nil {
|
||||
return fmt.Errorf("invalid conditions: %w", err)
|
||||
}
|
||||
if err := s.normalizeAutomationRuleAttachmentActions(ctx, rule.AccountID, &rule.Actions); err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate actions before saving
|
||||
if err := ValidateActions(rule.Actions); err != nil {
|
||||
return fmt.Errorf("invalid actions: %w", err)
|
||||
@@ -130,12 +147,100 @@ func (s *AutomationRuleService) UpdateForAccount(ctx context.Context, accountID
|
||||
existing.Description = rule.Description
|
||||
existing.EventName = rule.EventName
|
||||
existing.Conditions = NormalizeConditions(rule.Conditions)
|
||||
existing.Actions = rule.Actions
|
||||
if rule.Actions != nil {
|
||||
if err := s.normalizeAutomationRuleAttachmentActions(ctx, accountID, &rule.Actions); err != nil {
|
||||
return err
|
||||
}
|
||||
existing.Actions = rule.Actions
|
||||
}
|
||||
existing.Active = rule.Active
|
||||
|
||||
return s.Update(ctx, &existing)
|
||||
}
|
||||
|
||||
func (s *AutomationRuleService) normalizeAutomationRuleAttachmentActions(ctx context.Context, accountID uint, actions *Actions) error {
|
||||
if actions == nil {
|
||||
return nil
|
||||
}
|
||||
for i := range *actions {
|
||||
action := &(*actions)[i]
|
||||
if action.ActionName != "send_attachment" {
|
||||
continue
|
||||
}
|
||||
if action.ActionParams == nil {
|
||||
return fmt.Errorf("invalid attachment")
|
||||
}
|
||||
upload, err := s.findAutomationRuleUpload(ctx, accountID, firstAutomationRuleBlobID(action.ActionParams))
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid attachment")
|
||||
}
|
||||
action.ActionParams["blob_id"] = upload.ID
|
||||
delete(action.ActionParams, "attachment_url")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstAutomationRuleBlobID(params map[string]interface{}) interface{} {
|
||||
if value := params["blob_id"]; value != nil {
|
||||
return value
|
||||
}
|
||||
return params["attachment_url"]
|
||||
}
|
||||
|
||||
func (s *AutomationRuleService) findAutomationRuleUpload(ctx context.Context, accountID uint, value interface{}) (*model.DirectUpload, error) {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
if parsed, err := strconv.ParseUint(v, 10, 64); err == nil {
|
||||
return s.findAutomationRuleUploadByID(ctx, accountID, uint(parsed))
|
||||
}
|
||||
var upload model.DirectUpload
|
||||
if err := s.db.DB().WithContext(ctx).Where("account_id = ? AND upload_uuid = ?", accountID, v).First(&upload).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &upload, nil
|
||||
case float64:
|
||||
return s.findAutomationRuleUploadByID(ctx, accountID, uint(v))
|
||||
case int:
|
||||
return s.findAutomationRuleUploadByID(ctx, accountID, uint(v))
|
||||
case uint:
|
||||
return s.findAutomationRuleUploadByID(ctx, accountID, v)
|
||||
case json.Number:
|
||||
parsed, err := strconv.ParseUint(string(v), 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.findAutomationRuleUploadByID(ctx, accountID, uint(parsed))
|
||||
default:
|
||||
return nil, gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
func (s *AutomationRuleService) findAutomationRuleUploadByID(ctx context.Context, accountID, id uint) (*model.DirectUpload, error) {
|
||||
var upload model.DirectUpload
|
||||
if err := s.db.DB().WithContext(ctx).Where("account_id = ? AND id = ?", accountID, id).First(&upload).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &upload, nil
|
||||
}
|
||||
|
||||
func (s *AutomationRuleService) hydrateAutomationRuleFiles(ctx context.Context, rule *AutomationRule) error {
|
||||
ids := macroAttachmentBlobIDs(rule.Actions)
|
||||
if len(ids) == 0 {
|
||||
rule.Files = nil
|
||||
return nil
|
||||
}
|
||||
var uploads []model.DirectUpload
|
||||
if err := s.db.DB().WithContext(ctx).Where("account_id = ? AND id IN ?", rule.AccountID, ids).Order("id ASC").Find(&uploads).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
files := make([]AutomationRuleFile, 0, len(uploads))
|
||||
for _, upload := range uploads {
|
||||
files = append(files, AutomationRuleFile{ID: upload.ID, AutomationRuleID: rule.ID, FileType: upload.MimeType, AccountID: upload.AccountID, FileURL: upload.FileURL, BlobID: upload.ID, Filename: upload.OriginalName})
|
||||
}
|
||||
rule.Files = files
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete soft-deletes an automation rule.
|
||||
func (s *AutomationRuleService) Delete(ctx context.Context, id uint) error {
|
||||
return s.db.DB().WithContext(ctx).Delete(&AutomationRule{}, id).Error
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
)
|
||||
|
||||
func TestAutomationRuleService_Create(t *testing.T) {
|
||||
@@ -34,6 +36,72 @@ func TestAutomationRuleService_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomationRuleService_CreateNormalizesAttachmentActionsAndHydratesFiles(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
db := dbProvider.DB()
|
||||
accountID, _ := seedTestAccount(db, t)
|
||||
upload := &model.DirectUpload{UploadUUID: "automation-service-upload", AccountID: accountID, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "rule.pdf", FileType: "file", MimeType: "application/pdf", FileSize: 789, FileURL: "/uploads/account/1/rule.pdf"}
|
||||
if err := db.Create(upload).Error; err != nil {
|
||||
t.Fatalf("create upload: %v", err)
|
||||
}
|
||||
svc := NewAutomationRuleService(dbProvider)
|
||||
|
||||
rule := &AutomationRule{
|
||||
AccountID: accountID,
|
||||
EventName: "conversation_created",
|
||||
Name: "Send attachment",
|
||||
Conditions: Conditions{},
|
||||
Actions: Actions{{ActionName: "send_attachment", ActionParams: map[string]interface{}{"blob_id": "automation-service-upload", "attachment_url": "stale"}}},
|
||||
Active: true,
|
||||
}
|
||||
if err := svc.Create(context.Background(), rule); err != nil {
|
||||
t.Fatalf("create rule: %v", err)
|
||||
}
|
||||
if got := macroBlobIDAsUint(rule.Actions[0].ActionParams["blob_id"]); got != upload.ID {
|
||||
t.Fatalf("expected blob id %d, got %d", upload.ID, got)
|
||||
}
|
||||
if _, ok := rule.Actions[0].ActionParams["attachment_url"]; ok {
|
||||
t.Fatal("expected attachment_url to be removed")
|
||||
}
|
||||
if len(rule.Files) != 1 || rule.Files[0].BlobID != upload.ID || rule.Files[0].AutomationRuleID != rule.ID {
|
||||
t.Fatalf("unexpected files payload: %#v", rule.Files)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomationRuleService_MatchAndExecuteCreatesAttachmentMessages(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
db := dbProvider.DB()
|
||||
accountID, _ := seedTestAccount(db, t)
|
||||
inboxID := seedTestInbox(db, t, accountID)
|
||||
contactID := seedTestContact(db, t, accountID)
|
||||
conversationID := seedTestConversationWithDetails(db, t, accountID, inboxID, contactID, "open", "low", "web", 0)
|
||||
upload := &model.DirectUpload{UploadUUID: "automation-exec-upload", AccountID: accountID, Status: model.DirectUploadStatusPending, Source: model.DirectUploadSourceAccount, OriginalName: "automation.pdf", FileType: "file", MimeType: "application/pdf", FileSize: 456, FileURL: "/uploads/account/1/automation.pdf"}
|
||||
if err := db.Create(upload).Error; err != nil {
|
||||
t.Fatalf("create upload: %v", err)
|
||||
}
|
||||
svc := NewAutomationRuleService(dbProvider)
|
||||
rule := &AutomationRule{AccountID: accountID, EventName: "conversation_created", Name: "file rule", Conditions: Conditions{}, Actions: Actions{{ActionName: "send_attachment", ActionParams: map[string]interface{}{"blob_id": "automation-exec-upload"}}}, Active: true}
|
||||
if err := svc.Create(context.Background(), rule); err != nil {
|
||||
t.Fatalf("create rule: %v", err)
|
||||
}
|
||||
|
||||
if err := svc.MatchAndExecute(context.Background(), accountID, "conversation_created", conversationID, map[string]interface{}{}); err != nil {
|
||||
t.Fatalf("execute automation: %v", err)
|
||||
}
|
||||
|
||||
var message model.Message
|
||||
if err := db.Where("conversation_id = ? AND content_type = ?", conversationID, "file").First(&message).Error; err != nil {
|
||||
t.Fatalf("expected file message: %v", err)
|
||||
}
|
||||
var attachment model.Attachment
|
||||
if err := db.Where("message_id = ?", message.ID).First(&attachment).Error; err != nil {
|
||||
t.Fatalf("expected attachment: %v", err)
|
||||
}
|
||||
if attachment.FileURL != upload.FileURL || attachment.FileName != upload.OriginalName {
|
||||
t.Fatalf("unexpected attachment: %#v", attachment)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAutomationRuleService_GetByID(t *testing.T) {
|
||||
dbProvider := setupAutomationTestDBProvider(t)
|
||||
accountID, _ := seedTestAccount(dbProvider.DB(), t)
|
||||
|
||||
@@ -3,6 +3,7 @@ package v1
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gochat/gochat/internal/automation"
|
||||
@@ -33,7 +34,7 @@ type automationRuleRequest struct {
|
||||
EventName string `json:"event_name"`
|
||||
Active *bool `json:"active"`
|
||||
Conditions []automationRuleConditionRequest `json:"conditions"`
|
||||
Actions []automationRuleActionRequest `json:"actions"`
|
||||
Actions *[]automationRuleActionRequest `json:"actions"`
|
||||
}
|
||||
|
||||
type automationRuleConditionRequest struct {
|
||||
@@ -120,6 +121,10 @@ func (h *AutomationRuleHandler) Create(c *gin.Context) {
|
||||
}
|
||||
|
||||
if svcErr := h.svc.Create(c.Request.Context(), rule); svcErr != nil {
|
||||
if strings.Contains(svcErr.Error(), "invalid attachment") {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid attachment"})
|
||||
return
|
||||
}
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
@@ -167,6 +172,10 @@ func (h *AutomationRuleHandler) Update(c *gin.Context) {
|
||||
rule.ID = automationID
|
||||
|
||||
if svcErr := h.svc.UpdateForAccount(c.Request.Context(), accountID, rule); svcErr != nil {
|
||||
if strings.Contains(svcErr.Error(), "invalid attachment") {
|
||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "invalid attachment"})
|
||||
return
|
||||
}
|
||||
handleServiceError(c, svcErr)
|
||||
return
|
||||
}
|
||||
@@ -325,13 +334,16 @@ func buildAutomationRuleFromRequest(accountID uint, req automationRuleRequest) (
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
var actions automation.Actions
|
||||
if req.Actions != nil {
|
||||
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})
|
||||
}
|
||||
actions = append(actions, automation.Action{ActionName: actionReq.ActionName, ActionParams: params})
|
||||
}
|
||||
|
||||
return &automation.AutomationRule{
|
||||
@@ -354,7 +366,7 @@ func serializeAutomationRules(rules []automation.AutomationRule) []gin.H {
|
||||
}
|
||||
|
||||
func serializeAutomationRule(rule *automation.AutomationRule) gin.H {
|
||||
return gin.H{
|
||||
serialized := gin.H{
|
||||
"id": rule.ID,
|
||||
"account_id": rule.AccountID,
|
||||
"name": rule.Name,
|
||||
@@ -365,6 +377,26 @@ func serializeAutomationRule(rule *automation.AutomationRule) gin.H {
|
||||
"created_on": rule.CreatedAt.Unix(),
|
||||
"active": rule.Active,
|
||||
}
|
||||
if len(rule.Files) > 0 {
|
||||
serialized["files"] = serializeAutomationRuleFiles(rule.Files)
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
func serializeAutomationRuleFiles(files []automation.AutomationRuleFile) []gin.H {
|
||||
result := make([]gin.H, 0, len(files))
|
||||
for _, file := range files {
|
||||
result = append(result, gin.H{
|
||||
"id": file.ID,
|
||||
"automation_rule_id": file.AutomationRuleID,
|
||||
"file_type": file.FileType,
|
||||
"account_id": file.AccountID,
|
||||
"file_url": file.FileURL,
|
||||
"blob_id": file.BlobID,
|
||||
"filename": file.Filename,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func serializeAutomationConditions(conditions automation.Conditions) []gin.H {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/gochat/gochat/internal/automation"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
)
|
||||
|
||||
// automationDBProvider wraps *gorm.DB to implement automation.DBProvider.
|
||||
@@ -40,7 +41,7 @@ func (s *AutomationRuleHandlerTestSuite) SetupSuite() {
|
||||
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.Require().NoError(db.AutoMigrate(&automation.AutomationRule{}, &model.DirectUpload{}), "failed to auto-migrate AutomationRule model")
|
||||
s.db = db
|
||||
|
||||
provider := &automationDBProvider{db: db}
|
||||
@@ -74,6 +75,7 @@ func (s *AutomationRuleHandlerTestSuite) TearDownSuite() {
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) SetupTest() {
|
||||
s.db.Exec("DELETE FROM automation_rules")
|
||||
s.db.Exec("DELETE FROM direct_uploads")
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) createRule(accountID uint, eventName, name string, active bool) *automation.AutomationRule {
|
||||
@@ -192,6 +194,53 @@ func (s *AutomationRuleHandlerTestSuite) TestCreate_Success() {
|
||||
s.Equal(float64(1), saved.Actions[0].ActionParams["team_id"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestCreate_AttachmentActionAcceptsSignedUploadAndSerializesFiles() {
|
||||
upload := &model.DirectUpload{
|
||||
UploadUUID: "automation-upload-signed-id",
|
||||
AccountID: 1,
|
||||
Status: model.DirectUploadStatusPending,
|
||||
Source: model.DirectUploadSourceAccount,
|
||||
OriginalName: "automation.pdf",
|
||||
FileType: "file",
|
||||
MimeType: "application/pdf",
|
||||
FileSize: 321,
|
||||
FileURL: "/uploads/account/1/automation.pdf",
|
||||
}
|
||||
s.Require().NoError(s.db.Create(upload).Error)
|
||||
body := `{"event_name":"conversation_created","name":"Send file","active":true,"conditions":[],"actions":[{"action_name":"send_attachment","action_params":["automation-upload-signed-id"]}]}`
|
||||
|
||||
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.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
action := resp["actions"].([]interface{})[0].(map[string]interface{})
|
||||
s.Equal([]interface{}{float64(upload.ID)}, action["action_params"])
|
||||
files := resp["files"].([]interface{})
|
||||
s.Require().Len(files, 1)
|
||||
file := files[0].(map[string]interface{})
|
||||
s.Equal(float64(upload.ID), file["blob_id"])
|
||||
s.Equal(resp["id"], file["automation_rule_id"])
|
||||
s.Equal("automation.pdf", file["filename"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestCreate_AttachmentActionRejectsInvalidBlob() {
|
||||
body := `{"event_name":"conversation_created","name":"Bad file","active":true,"conditions":[],"actions":[{"action_name":"send_attachment","action_params":["missing-upload"]}]}`
|
||||
|
||||
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.StatusUnprocessableEntity, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
s.Equal("invalid attachment", resp["error"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestCreate_InvalidJSON() {
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(`{invalid`))
|
||||
@@ -234,6 +283,28 @@ func (s *AutomationRuleHandlerTestSuite) TestUpdate_Success() {
|
||||
s.Equal("UpdatedName", saved.Name)
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestUpdate_OmittedActionsPreservesExistingActions() {
|
||||
rule := s.createRule(1, "conversation_created", "KeepActions", true)
|
||||
|
||||
body := `{"event_name":"message_created","name":"Renamed","active":false,"conditions":[{"attribute_key":"status","filter_operator":"equal_to","values":["resolved"],"query_operator":"and"}]}`
|
||||
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.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
payload := resp["payload"].(map[string]interface{})
|
||||
action := payload["actions"].([]interface{})[0].(map[string]interface{})
|
||||
s.Equal("assign_team", action["action_name"])
|
||||
s.Equal([]interface{}{float64(1)}, action["action_params"])
|
||||
|
||||
var saved automation.AutomationRule
|
||||
s.Require().NoError(s.db.First(&saved, rule.ID).Error)
|
||||
s.Equal(float64(1), saved.Actions[0].ActionParams["team_id"])
|
||||
}
|
||||
|
||||
func (s *AutomationRuleHandlerTestSuite) TestUpdate_InvalidID() {
|
||||
body := `{"name":"Updated","active":true}`
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
Reference in New Issue
Block a user