package v1 import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "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/campaign" "github.com/gochat/gochat/internal/model" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // marshalNested wraps body under the given key to match Chatwoot params.require(:model) format. // e.g. marshalNested("campaign", body) → {"campaign": {...}} func marshalNested(key string, body map[string]interface{}) []byte { wrapped := map[string]interface{}{key: body} b, _ := json.Marshal(wrapped) return b } // CampaignHandlerTestSuite tests CampaignHandler CRUD + lifecycle methods // with a real SQLite database and wired services. type CampaignHandlerTestSuite struct { suite.Suite db *gorm.DB router *gin.Engine handler *CampaignHandler account *model.Account inbox *model.Inbox // Counter for unique DisplayID displayIDCounter uint } func (s *CampaignHandlerTestSuite) nextDisplayID() uint { s.displayIDCounter++ return s.displayIDCounter } // SetupSuite initializes the database, services, handler, and test data. func (s *CampaignHandlerTestSuite) 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( &model.Account{}, &model.Inbox{}, &campaign.Campaign{}, ), "failed to auto-migrate models") s.db = db // Wire repos → services → handler campaignRepo := repository.NewCampaignRepo(db) campaignSvc := campaign.NewCampaignService(db) svc := service.NewCampaignService(campaignSvc, campaignRepo) s.handler = NewCampaignHandler(svc) // Setup router r := gin.New() r.Use(gin.Recovery(), s.mockAuthMiddleware()) accountGroup := r.Group("/api/v1/accounts/:account_id") { campaigns := accountGroup.Group("/campaigns") { campaigns.GET("", s.handler.List) campaigns.GET("/:campaign_id", s.handler.Get) campaigns.POST("", s.handler.Create) campaigns.PATCH("/:campaign_id", s.handler.Update) campaigns.PUT("/:campaign_id", s.handler.Update) campaigns.DELETE("/:campaign_id", s.handler.Delete) campaigns.POST("/:campaign_id/start", s.handler.Start) campaigns.POST("/:campaign_id/stop", s.handler.Stop) } } s.router = r // Create test account s.account = &model.Account{Name: "CampaignTestOrg", Locale: "en", Active: true} s.Require().NoError(db.Create(s.account).Error) // Create test inbox (channel_id=1, web_widget) s.inbox = &model.Inbox{AccountID: s.account.ID, Name: "CampaignTestInbox", ChannelType: "web_widget", ChannelID: 1} s.Require().NoError(db.Create(s.inbox).Error) } func (s *CampaignHandlerTestSuite) TearDownSuite() { if s.db != nil { sqlDB, _ := s.db.DB() sqlDB.Close() } } // SetupTest resets data between tests. func (s *CampaignHandlerTestSuite) SetupTest() { s.db.Exec("DELETE FROM campaigns") s.db.Exec("DELETE FROM inboxes") s.db.Exec("DELETE FROM accounts") s.displayIDCounter = 0 // Re-seed base data s.account = &model.Account{Name: "CampaignTestOrg", Locale: "en", Active: true} s.Require().NoError(s.db.Create(s.account).Error) s.inbox = &model.Inbox{AccountID: s.account.ID, Name: "CampaignTestInbox", ChannelType: "web_widget", ChannelID: 1} s.Require().NoError(s.db.Create(s.inbox).Error) } // mockAuthMiddleware sets account_id in the Gin context via the URL param. // Since getAccountID reads account_id from the URL param first, // the :account_id route param will provide it automatically. // We also set user_id for completeness. func (s *CampaignHandlerTestSuite) mockAuthMiddleware() gin.HandlerFunc { return func(c *gin.Context) { c.Set("user_id", uint(1)) c.Next() } } // Helper: build account URL prefix func (s *CampaignHandlerTestSuite) accountURL() string { return "/api/v1/accounts/" + strconv.FormatUint(uint64(s.account.ID), 10) + "/campaigns" } func (s *CampaignHandlerTestSuite) seedInbox(channelType string) *model.Inbox { inbox := &model.Inbox{AccountID: s.account.ID, Name: "Campaign " + channelType + " Inbox", ChannelType: channelType, ChannelID: 1} s.Require().NoError(s.db.Create(inbox).Error) return inbox } // Helper: seed a campaign directly into the DB for Get/List/Delete/Update tests func (s *CampaignHandlerTestSuite) seedCampaign(title, message, campaignType string) *campaign.Campaign { c := &campaign.Campaign{ AccountID: s.account.ID, InboxID: s.inbox.ID, DisplayID: s.nextDisplayID(), Title: title, Message: message, CampaignStatus: campaign.CampaignStatusActive, CampaignType: campaign.CampaignType(campaignType), Audience: "{}", TriggerRules: "{}", TemplateParams: "{}", Enabled: true, } s.Require().NoError(s.db.Create(c).Error) return c } // ========== List Tests ========== func (s *CampaignHandlerTestSuite) TestList_Success() { s.seedCampaign("Test Campaign 1", "Hello world", "ongoing") s.seedCampaign("Test Campaign 2", "Welcome", "one_off") w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"?page=1&per_page=25", nil) 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)) s.Len(resp, 2) s.NotContains(resp[0], "success") s.NotContains(resp[0], "data") s.Contains(resp[0], "inbox") } func (s *CampaignHandlerTestSuite) TestList_Empty() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"?page=1&per_page=25", nil) 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)) s.Empty(resp) } func (s *CampaignHandlerTestSuite) TestList_Unauthorized() { // Route without :account_id param → getAccountID returns 0 r := gin.New() r.Use(gin.Recovery()) r.GET("/api/v1/accounts/campaigns", s.handler.List) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/campaigns?page=1&per_page=25", nil) r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Get Tests ========== func (s *CampaignHandlerTestSuite) TestGet_Success() { c := s.seedCampaign("GetTest Campaign", "Test message", "ongoing") w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/"+strconv.FormatUint(uint64(c.DisplayID), 10), nil) 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)) s.NotContains(resp, "success") s.NotContains(resp, "data") s.Equal(c.Title, resp["title"]) s.Equal(float64(c.DisplayID), resp["id"]) s.Contains(resp, "inbox") } func (s *CampaignHandlerTestSuite) TestGet_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/99999", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } func (s *CampaignHandlerTestSuite) TestGet_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/abc", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestGet_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.GET("/api/v1/campaigns/:campaign_id", s.handler.Get) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/campaigns/1", nil) r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Create Tests ========== func (s *CampaignHandlerTestSuite) TestCreate_Success() { smsInbox := s.seedInbox("Channel::Sms") body := map[string]interface{}{ "inbox_id": smsInbox.ID, "title": "New Campaign", "message": "Hello from campaign", "enabled": true, "scheduled_at": "2026-06-07T10:30:00Z", "audience": []map[string]interface{}{{"type": "Label", "id": 1}}, "trigger_rules": map[string]interface{}{"url": "https://example.com"}, "template_params": map[string]interface{}{"name": "value"}, } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL(), bytes.NewReader(bodyBytes)) 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)) s.NotContains(resp, "success") s.NotContains(resp, "data") s.Equal("New Campaign", resp["title"]) s.Equal(float64(s.account.ID), resp["account_id"]) s.Equal("one_off", resp["campaign_type"]) s.Equal(float64(1780828200), resp["scheduled_at"]) s.Greater(resp["id"].(float64), float64(0)) s.NotNil(resp["inbox"]) s.Contains(resp, "audience") s.Contains(resp, "template_params") } func (s *CampaignHandlerTestSuite) TestCreate_LiveChatDefaultsOngoingWithoutCampaignType() { body := map[string]interface{}{ "inbox_id": s.inbox.ID, "title": "Live Chat Campaign", "message": "Hello from live chat", "enabled": true, "scheduled_at": "2026-06-07T10:30:00Z", "trigger_rules": map[string]interface{}{"url": "https://example.com", "time_on_page": 10}, } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL(), bytes.NewReader(bodyBytes)) 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)) s.Equal("Live Chat Campaign", resp["title"]) s.Equal("ongoing", resp["campaign_type"]) s.NotContains(resp, "scheduled_at") s.NotContains(resp, "audience") } func (s *CampaignHandlerTestSuite) TestCreate_ValidationError() { // Missing required fields (title, message, inbox_id) body := map[string]interface{}{ "campaign_type": "ongoing", } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL(), bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) // Validation failure → service returns error → handleServiceError → 400 or 500 s.True(w.Code == http.StatusBadRequest || w.Code == http.StatusUnprocessableEntity) } func (s *CampaignHandlerTestSuite) TestCreate_InvalidJSON() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL(), bytes.NewReader([]byte("invalid json"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestCreate_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.POST("/api/v1/campaigns", s.handler.Create) // no :account_id param body := map[string]interface{}{ "title": "Test", "message": "msg", "campaign_type": "ongoing", } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/campaigns", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Update Tests ========== func (s *CampaignHandlerTestSuite) TestUpdate_Success() { c := s.seedCampaign("Original Title", "Original message", "ongoing") smsInbox := s.seedInbox("Channel::Sms") body := map[string]interface{}{ "title": "Updated Title", "message": "Updated message", "inbox_id": smsInbox.ID, "scheduled_at": "2026-06-07T10:30:00Z", } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("PATCH", s.accountURL()+"/"+strconv.FormatUint(uint64(c.DisplayID), 10), bytes.NewReader(bodyBytes)) 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)) s.NotContains(resp, "success") s.Equal("Updated Title", resp["title"]) s.Equal("one_off", resp["campaign_type"]) s.Equal(float64(1780828200), resp["scheduled_at"]) var updated campaign.Campaign s.Require().NoError(s.db.First(&updated, c.ID).Error) s.Equal(smsInbox.ID, updated.InboxID) s.Require().NotNil(updated.ScheduledAt) s.Equal(int64(1780828200), updated.ScheduledAt.Unix()) } func (s *CampaignHandlerTestSuite) TestUpdate_NotFound() { body := map[string]interface{}{ "title": "Updated Title", } bodyBytes := marshalNested("campaign", body) w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.accountURL()+"/99999", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } func (s *CampaignHandlerTestSuite) TestUpdate_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.accountURL()+"/abc", bytes.NewReader([]byte(`{"title":"X"}`))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestUpdate_InvalidJSON() { c := s.seedCampaign("Original Title", "Original message", "ongoing") w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", s.accountURL()+"/"+strconv.FormatUint(uint64(c.ID), 10), bytes.NewReader([]byte("invalid"))) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestUpdate_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.PUT("/api/v1/campaigns/:campaign_id", s.handler.Update) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("PUT", "/api/v1/campaigns/1", bytes.NewReader([]byte(`{"title":"X"}`))) req.Header.Set("Content-Type", "application/json") r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Delete Tests ========== func (s *CampaignHandlerTestSuite) TestDelete_Success() { c := s.seedCampaign("DeleteTest Campaign", "Test message", "ongoing") w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.accountURL()+"/"+strconv.FormatUint(uint64(c.DisplayID), 10), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) // Verify the campaign is soft-deleted var count int64 s.db.Model(&campaign.Campaign{}).Where("id = ? AND deleted_at IS NULL", c.ID).Count(&count) s.Equal(int64(0), count) } func (s *CampaignHandlerTestSuite) TestDelete_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.accountURL()+"/99999", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } func (s *CampaignHandlerTestSuite) TestDelete_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", s.accountURL()+"/abc", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestDelete_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.DELETE("/api/v1/campaigns/:campaign_id", s.handler.Delete) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("DELETE", "/api/v1/campaigns/1", nil) r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Start Tests ========== func (s *CampaignHandlerTestSuite) TestStart_Success() { c := s.seedCampaign("StartTest Campaign", "Test message", "ongoing") // Ensure campaign is enabled (default is true) s.db.Model(&campaign.Campaign{}).Where("id = ?", c.ID).Update("enabled", true) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/"+strconv.FormatUint(uint64(c.ID), 10)+"/start", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) var resp struct { Success bool `json:"success"` Data map[string]interface{} `json:"data"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) s.True(resp.Success) s.Equal("campaign triggered successfully", resp.Data["message"]) } func (s *CampaignHandlerTestSuite) TestStart_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/99999/start", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } func (s *CampaignHandlerTestSuite) TestStart_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/abc/start", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestStart_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.POST("/api/v1/campaigns/:campaign_id/start", s.handler.Start) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/campaigns/1/start", nil) r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Stop Tests ========== func (s *CampaignHandlerTestSuite) TestStop_Success() { c := s.seedCampaign("StopTest Campaign", "Test message", "ongoing") w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/"+strconv.FormatUint(uint64(c.ID), 10)+"/stop", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) var resp struct { Success bool `json:"success"` Data map[string]interface{} `json:"data"` } s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &resp)) s.True(resp.Success) s.Equal("campaign stopped successfully", resp.Data["message"]) // Verify campaign status changed to completed var updated campaign.Campaign s.db.First(&updated, c.ID) s.Equal(campaign.CampaignStatusCompleted, updated.CampaignStatus) } func (s *CampaignHandlerTestSuite) TestStop_NotFound() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/99999/stop", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } func (s *CampaignHandlerTestSuite) TestStop_InvalidID() { w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/abc/stop", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusBadRequest, w.Code) } func (s *CampaignHandlerTestSuite) TestStop_Unauthorized() { r := gin.New() r.Use(gin.Recovery()) r.POST("/api/v1/campaigns/:campaign_id/stop", s.handler.Stop) // no :account_id param w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/campaigns/1/stop", nil) r.ServeHTTP(w, req) s.Equal(http.StatusUnauthorized, w.Code) } // ========== Lifecycle: Start then Stop ========== func (s *CampaignHandlerTestSuite) TestLifecycle_StartThenStop() { c := s.seedCampaign("Lifecycle Campaign", "Hello lifecycle", "one_off") // Start w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL()+"/"+strconv.FormatUint(uint64(c.ID), 10)+"/start", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) // Stop w = httptest.NewRecorder() req, _ = http.NewRequest("POST", s.accountURL()+"/"+strconv.FormatUint(uint64(c.ID), 10)+"/stop", nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) // Verify campaign is completed var updated campaign.Campaign s.db.First(&updated, c.ID) s.Equal(campaign.CampaignStatusCompleted, updated.CampaignStatus) } // ========== Full CRUD Lifecycle ========== func (s *CampaignHandlerTestSuite) TestCRUD_FullLifecycle() { // Create createBody := map[string]interface{}{ "inbox_id": s.inbox.ID, "title": "Lifecycle Campaign", "message": "Test lifecycle message", "campaign_type": "one_off", "enabled": true, } createBytes := marshalNested("campaign", createBody) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", s.accountURL(), bytes.NewReader(createBytes)) 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)) createdID := uint(createResp["id"].(float64)) // Get w = httptest.NewRecorder() req, _ = http.NewRequest("GET", s.accountURL()+"/"+strconv.FormatUint(uint64(createdID), 10), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) var getResp map[string]interface{} s.Require().NoError(json.Unmarshal(w.Body.Bytes(), &getResp)) s.Equal("Lifecycle Campaign", getResp["title"]) // Update updateBody := map[string]interface{}{ "title": "Updated Lifecycle", "message": "Updated lifecycle message", } updateBytes := marshalNested("campaign", updateBody) w = httptest.NewRecorder() req, _ = http.NewRequest("PATCH", s.accountURL()+"/"+strconv.FormatUint(uint64(createdID), 10), bytes.NewReader(updateBytes)) 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)) s.Equal("Updated Lifecycle", updateResp["title"]) // List (should include our campaign) w = httptest.NewRecorder() req, _ = http.NewRequest("GET", s.accountURL()+"?page=1&per_page=25", 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)) s.GreaterOrEqual(len(listResp), 1) // Delete w = httptest.NewRecorder() req, _ = http.NewRequest("DELETE", s.accountURL()+"/"+strconv.FormatUint(uint64(createdID), 10), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusOK, w.Code) // Get after delete → should be not found (soft delete) w = httptest.NewRecorder() req, _ = http.NewRequest("GET", s.accountURL()+"/"+strconv.FormatUint(uint64(createdID), 10), nil) s.router.ServeHTTP(w, req) s.Equal(http.StatusNotFound, w.Code) } // ========== Cross-Account Isolation ========== func (s *CampaignHandlerTestSuite) TestGet_DifferentAccount() { // Create another account and campaign otherAccount := &model.Account{Name: "OtherOrg", Locale: "en", Active: true} s.Require().NoError(s.db.Create(otherAccount).Error) otherInbox := &model.Inbox{AccountID: otherAccount.ID, Name: "OtherInbox", ChannelType: "web_widget", ChannelID: 2} s.Require().NoError(s.db.Create(otherInbox).Error) otherCampaign := &campaign.Campaign{ AccountID: otherAccount.ID, InboxID: otherInbox.ID, DisplayID: s.nextDisplayID(), Title: "Other Account Campaign", Message: "Not visible", CampaignStatus: campaign.CampaignStatusActive, CampaignType: campaign.CampaignTypeOngoing, Audience: "{}", TriggerRules: "{}", TemplateParams: "{}", Enabled: true, } s.Require().NoError(s.db.Create(otherCampaign).Error) // Try to get other account's campaign using our account's URL w := httptest.NewRecorder() req, _ := http.NewRequest("GET", s.accountURL()+"/"+strconv.FormatUint(uint64(otherCampaign.ID), 10), nil) s.router.ServeHTTP(w, req) // Should be not found because it belongs to a different account s.Equal(http.StatusNotFound, w.Code) } // ========== Run the suite ========== func TestCampaignHandlerTestSuite(t *testing.T) { suite.Run(t, new(CampaignHandlerTestSuite)) }