Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
546 lines
22 KiB
Go
546 lines
22 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"
|
|
"github.com/gochat/gochat/internal/model"
|
|
)
|
|
|
|
// 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{}, &model.DirectUpload{}), "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")
|
|
s.db.Exec("DELETE FROM direct_uploads")
|
|
}
|
|
|
|
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_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`))
|
|
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_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) TestChatwootFrontendCRUDCloneTogglePayloadsAndValidation() {
|
|
createBody := `{"event_name":"conversation_created","name":"Frontend Rule","description":"Created from Woochat","active":true,"conditions":[{"attribute_key":"status","filter_operator":"equal_to","values":["open"],"query_operator":"and"}],"actions":[{"action_name":"add_label","action_params":["vip","trial"]}]}`
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(createBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
|
|
var createResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &createResp))
|
|
s.assertChatwootAutomationRulePayload(createResp, "Frontend Rule", true)
|
|
s.Equal("conversation_created", createResp["event_name"])
|
|
condition := createResp["conditions"].([]interface{})[0].(map[string]interface{})
|
|
s.Equal("status", condition["attribute_key"])
|
|
s.Equal("equal_to", condition["filter_operator"])
|
|
s.NotContains(condition, "attribute")
|
|
action := createResp["actions"].([]interface{})[0].(map[string]interface{})
|
|
s.Equal("add_label", action["action_name"])
|
|
s.Equal([]interface{}{"vip", "trial"}, action["action_params"])
|
|
ruleID := uint(createResp["id"].(float64))
|
|
|
|
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 listResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &listResp))
|
|
rules := listResp["payload"].([]interface{})
|
|
s.Require().Len(rules, 1)
|
|
s.assertChatwootAutomationRulePayload(rules[0].(map[string]interface{}), "Frontend Rule", true)
|
|
|
|
updateBody := `{"event_name":"message_created","name":"Frontend Rule Updated","description":"Updated from Woochat","active":false,"conditions":[{"attribute_key":"content","filter_operator":"contains","values":["refund"],"query_operator":"and"}],"actions":[{"action_name":"change_status","action_params":["resolved"]}]}`
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", ruleID), bytes.NewBufferString(updateBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
var updateResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &updateResp))
|
|
updatedPayload := updateResp["payload"].(map[string]interface{})
|
|
s.assertChatwootAutomationRulePayload(updatedPayload, "Frontend Rule Updated", false)
|
|
s.Equal("message_created", updatedPayload["event_name"])
|
|
s.Equal([]interface{}{"resolved"}, updatedPayload["actions"].([]interface{})[0].(map[string]interface{})["action_params"])
|
|
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/clone", ruleID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
var cloneResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &cloneResp))
|
|
clonePayload := cloneResp["payload"].(map[string]interface{})
|
|
s.assertChatwootAutomationRulePayload(clonePayload, "Frontend Rule Updated (copy)", false)
|
|
s.NotEqual(float64(ruleID), clonePayload["id"])
|
|
|
|
toggleBody := `{"active":true}`
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d/toggle_active", ruleID), bytes.NewBufferString(toggleBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
var toggleResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &toggleResp))
|
|
s.assertChatwootAutomationRulePayload(toggleResp["payload"].(map[string]interface{}), "Frontend Rule Updated", true)
|
|
|
|
invalidBody := `{"event_name":"conversation_created","active":true}`
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodPost, "/api/v1/accounts/1/automation_rules", bytes.NewBufferString(invalidBody))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusUnprocessableEntity, w.Code)
|
|
var validationResp map[string]interface{}
|
|
s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &validationResp))
|
|
validationError := validationResp["error"].(map[string]interface{})
|
|
s.Equal("VALIDATION_ERROR", validationError["code"])
|
|
s.Contains(validationError["message"], "name and event_name are required")
|
|
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/v1/accounts/1/automation_rules/%d", ruleID), nil)
|
|
s.router.ServeHTTP(w, req)
|
|
s.Equal(http.StatusOK, w.Code)
|
|
s.Empty(w.Body.String())
|
|
}
|
|
|
|
func (s *AutomationRuleHandlerTestSuite) assertChatwootAutomationRulePayload(payload map[string]interface{}, name string, active bool) {
|
|
s.NotContains(payload, "success")
|
|
s.NotContains(payload, "data")
|
|
s.Contains(payload, "id")
|
|
s.Equal(float64(1), payload["account_id"])
|
|
s.Equal(name, payload["name"])
|
|
s.Equal(active, payload["active"])
|
|
s.Contains(payload, "description")
|
|
s.Contains(payload, "event_name")
|
|
s.Contains(payload, "conditions")
|
|
s.Contains(payload, "actions")
|
|
s.Contains(payload, "created_on")
|
|
}
|
|
|
|
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))
|
|
}
|