package v1 import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "strconv" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "github.com/gochat/gochat/internal/model" channelmodel "github.com/gochat/gochat/internal/model/channel" "github.com/gochat/gochat/internal/channel/whatsapp" "github.com/gochat/gochat/internal/repository" "github.com/gochat/gochat/internal/service" ) // ── Setup helpers ──────────────────────────────────────────────── func setupTwilioHandlerTest(t *testing.T) (*TwilioChannelHandler, *gorm.DB) { t.Helper() db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) require.NoError(t, err) require.NoError(t, db.AutoMigrate( &model.Account{}, &model.Inbox{}, &model.AgentBot{}, &model.AgentBotInbox{}, &model.WebhookSubscription{}, &channelmodel.ChannelTwilioSMS{}, )) t.Cleanup(func() { sqlDB, _ := db.DB() sqlDB.Close() }) twRepo := repository.NewChannelTwilioSMSRepo(db) twChannelSvc := service.NewChannelTwilioSMSService(twRepo) inboxRepo := repository.NewInboxRepo(db) agentBotInboxRepo := repository.NewAgentBotInboxRepo(db) agentBotRepo := repository.NewAgentBotRepo(db) campaignRepo := repository.NewCampaignRepo(db) webhookSubRepo := repository.NewWebhookSubscriptionRepo(db) waRepo := whatsapp.NewRepository(db) waService := whatsapp.NewWhatsAppService(waRepo) inboxSvc := service.NewInboxService(inboxRepo, agentBotInboxRepo, agentBotRepo, campaignRepo, webhookSubRepo, waService, waRepo) handler := NewTwilioChannelHandler(twChannelSvc, inboxSvc, twRepo) // Seed an account account := &model.Account{Name: "TwilioTestOrg", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) return handler, db } func twilioAccountID(db *gorm.DB) string { var account model.Account db.First(&account) return strconv.FormatUint(uint64(account.ID), 10) } func twilioAccountIDUint(db *gorm.DB) uint { var account model.Account db.First(&account) return account.ID } func setupTwilioTestRouter(handler *TwilioChannelHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() ag := r.Group("/api/v1/accounts/:id") ag.POST("/channels/twilio_channel", handler.Create) ag.POST("/twilio_sms_channels", handler.Create) ag.GET("/twilio_sms_channels", handler.List) ig := ag.Group("/inboxes/:inbox_id/twilio_sms_channels") ig.GET("/:tw_id", handler.Get) ig.PATCH("/:tw_id", handler.Update) ig.DELETE("/:tw_id", handler.Delete) return r } // ── Create ────────────────────────────────────────────────────── func TestTwilioChannel_Create_Success(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) body := map[string]any{ "twilio_channel": map[string]any{ "account_sid": "ACtest123", "api_key_sid": "SKtest123", "auth_token": "authtoken_secret", "phone_number": "+15551234567", "messaging_service_sid": "MG123", "medium": "sms", "name": "Support SMS", }, } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+accountID+"/channels/twilio_channel", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "Support SMS", resp["name"]) assert.Equal(t, "Channel::TwilioSms", resp["channel_type"]) assert.Equal(t, "ACtest123", resp["account_sid"]) assert.Equal(t, "SKtest123", resp["api_key_sid"]) assert.Equal(t, "+15551234567", resp["phone_number"]) assert.Equal(t, "MG123", resp["messaging_service_sid"]) assert.Equal(t, "sms", resp["medium"]) require.NotContains(t, resp, "channel") require.NotContains(t, resp, "inbox") } func TestTwilioChannel_Create_InvalidAccountID(t *testing.T) { handler, _ := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) body := CreateTwilioSMSChannelRequest{ AccountSID: "ACtest123", PhoneNumber: "+15551234567", } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/invalid/twilio_sms_channels", bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "invalid account_id", resp["error"]) } func TestTwilioChannel_Create_InvalidRequestBody(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+accountID+"/twilio_sms_channels", bytes.NewReader([]byte("{bad json"))) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ── Get ────────────────────────────────────────────────────────── func TestTwilioChannel_Get_Success(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed a channel and its associated inbox. ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "ACget123", PhoneNumber: "+15559999999", AccountID: accountID, } require.NoError(t, db.Create(ch).Error) inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Get SMS", "twilio_sms", map[string]any{ "account_sid": "ACget123", "phone_number": "+15559999999", "medium": "sms", }) ch.InboxID = inbox.ID require.NoError(t, db.Save(ch).Error) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "Get SMS", resp["name"]) assert.Equal(t, "Channel::TwilioSms", resp["channel_type"]) assert.Equal(t, "ACget123", resp["account_sid"]) require.NotContains(t, resp, "channel") } func TestTwilioChannel_Get_InvalidID(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/inboxes/1/twilio_sms_channels/invalid", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "invalid tw_id", resp["error"]) } func TestTwilioChannel_Get_NotFound(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/inboxes/1/twilio_sms_channels/99999", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } // ── Update ────────────────────────────────────────────────────── func TestTwilioChannel_Update_Success(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed a channel and its associated inbox. ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "ACupd123", PhoneNumber: "+15558888888", AccountID: accountID, } require.NoError(t, db.Create(ch).Error) inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Update SMS", "twilio_sms", map[string]any{ "account_sid": "ACupd123", "phone_number": "+15558888888", "medium": "sms", }) ch.InboxID = inbox.ID require.NoError(t, db.Save(ch).Error) newPhone := "+15557777777" body := UpdateTwilioSMSChannelRequest{ PhoneNumber: &newPhone, } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) assert.Equal(t, "+15557777777", resp["phone_number"]) } func TestTwilioChannel_Update_WrongAccount(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed a channel owned by a different account ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "ACother", PhoneNumber: "+15556666666", AccountID: accountID + 999, // different account InboxID: 202, } require.NoError(t, db.Create(ch).Error) newPhone := "+15551111111" body := UpdateTwilioSMSChannelRequest{ PhoneNumber: &newPhone, } b, _ := json.Marshal(body) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), bytes.NewReader(b)) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestTwilioChannel_Update_InvalidTwID(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+accountID+"/inboxes/1/twilio_sms_channels/invalid", bytes.NewReader([]byte("{}"))) req.Header.Set("Content-Type", "application/json") router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } // ── Delete ────────────────────────────────────────────────────── func TestTwilioChannel_Delete_Success(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed a channel ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "ACdel123", PhoneNumber: "+15555555555", AccountID: accountID, InboxID: 203, } require.NoError(t, db.Create(ch).Error) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) // Verify channel is soft-deleted var count int64 db.Model(&channelmodel.ChannelTwilioSMS{}).Where("id = ?", ch.ID).Count(&count) assert.Equal(t, int64(0), count) } func TestTwilioChannel_Delete_WrongAccount(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed a channel owned by a different account ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "ACother2", PhoneNumber: "+15554444444", AccountID: accountID + 999, InboxID: 204, } require.NoError(t, db.Create(ch).Error) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/twilio_sms_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } func TestTwilioChannel_Delete_NotFound(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+accountID+"/inboxes/1/twilio_sms_channels/99999", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusNotFound, w.Code) } // ── List ────────────────────────────────────────────────────── func TestTwilioChannel_List_Success(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountIDUint(db) // Seed multiple channels and their associated inboxes. for i := 0; i < 3; i++ { ch := &channelmodel.ChannelTwilioSMS{ AccountSID: "AClist" + strconv.Itoa(i), PhoneNumber: "+1555" + strconv.Itoa(i) + "00000", AccountID: accountID, } require.NoError(t, db.Create(ch).Error) inbox := createTestChannelInbox(t, db, accountID, ch.ID, "SMS List "+strconv.Itoa(i), "twilio_sms", map[string]any{ "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "medium": "sms", }) ch.InboxID = inbox.ID require.NoError(t, db.Save(ch).Error) } w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/twilio_sms_channels", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) channels, ok := resp["payload"].([]interface{}) require.True(t, ok) assert.Len(t, channels, 3) } func TestTwilioChannel_List_InvalidAccountID(t *testing.T) { handler, _ := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/invalid/twilio_sms_channels", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) } func TestTwilioChannel_List_Empty(t *testing.T) { handler, db := setupTwilioHandlerTest(t) router := setupTwilioTestRouter(handler) accountID := twilioAccountID(db) w := httptest.NewRecorder() req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/twilio_sms_channels", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) var resp map[string]interface{} require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp)) channels, ok := resp["payload"].([]interface{}) require.True(t, ok) assert.Len(t, channels, 0) }