432 lines
15 KiB
Go
432 lines
15 KiB
Go
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)
|
|
}
|