feat(channels): align channel route inbox payloads

This commit is contained in:
2026-06-05 06:19:50 +08:00
parent 10c7df00f0
commit f04a03b65a
8 changed files with 426 additions and 189 deletions
@@ -0,0 +1,29 @@
package v1
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
)
func createTestChannelInbox(t *testing.T, db *gorm.DB, accountID, channelID uint, name, channelType string, channelConfig map[string]any) *model.Inbox {
t.Helper()
configJSON, err := json.Marshal(channelConfig)
require.NoError(t, err)
inbox := &model.Inbox{
AccountID: accountID,
Name: name,
ChannelType: channelType,
ChannelID: channelID,
Enabled: true,
ChannelConfig: string(configJSON),
}
require.NoError(t, db.Create(inbox).Error)
return inbox
}
@@ -12,7 +12,6 @@ package v1
import (
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -71,10 +70,8 @@ type CreateEmailChannelRequest struct {
// Create adds a new Email channel and creates the associated inbox.
// POST /api/v1/accounts/:id/channels/email_channel
func (h *EmailChannelHandler) Create(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid account_id: %v", err)
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
@@ -108,7 +105,7 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
SMTPLogin: req.SMTPLogin,
SMTPPassword: req.SMTPPassword,
SMTPSSLMode: req.SMTPSSLMode,
AccountID: uint(accountID),
AccountID: accountID,
}
if err := h.emailChannelSvc.Create(ctx, ch); err != nil {
@@ -131,7 +128,8 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
Enabled: true,
}
inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq)
inboxReq.Channel = emailChannelConfig(req)
inbox, err := h.inboxSvc.Create(ctx, accountID, inboxReq)
if err != nil {
applogger.L().Errorf("Failed to create inbox for Email channel: %v", err)
// Rollback channel creation
@@ -147,32 +145,32 @@ func (h *EmailChannelHandler) Create(c *gin.Context) {
if err := h.emailChannelSvc.Update(ctx, ch); err != nil {
applogger.L().Errorf("Failed to update Email channel with inbox_id: %v", err)
}
inbox, err = h.inboxSvc.BindChannel(ctx, accountID, inbox.ID, ch.ID, emailChannelConfig(req))
if err != nil {
applogger.L().Warnf("Failed to bind Email inbox channel config: %v", err)
}
c.JSON(http.StatusCreated, gin.H{
"channel": ch,
"inbox": inbox,
})
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// Get retrieves an Email channel by ID.
// GET /api/v1/accounts/:id/channels/email_channel/:em_id
func (h *EmailChannelHandler) Get(c *gin.Context) {
emIDStr := c.Param("em_id")
emID, err := strconv.ParseUint(emIDStr, 10, 64)
emID, err := parseUintParam(c, "em_id")
if err != nil {
applogger.L().Errorf("Invalid em_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
return
}
ch, err := h.emailChannelSvc.GetByID(c.Request.Context(), uint(emID))
ch, err := h.emailChannelSvc.GetByID(c.Request.Context(), emID)
if err != nil {
applogger.L().Errorf("Failed to get Email channel: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
return
}
c.JSON(http.StatusOK, gin.H{"channel": ch})
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
}
// UpdateEmailChannelRequest is the DTO for updating an Email channel.
@@ -198,15 +196,13 @@ type UpdateEmailChannelRequest struct {
// Update updates an Email channel configuration.
// PATCH /api/v1/accounts/:id/channels/email_channel/:em_id
func (h *EmailChannelHandler) Update(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
emIDStr := c.Param("em_id")
emID, err := strconv.ParseUint(emIDStr, 10, 64)
emID, err := parseUintParam(c, "em_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
return
@@ -214,7 +210,7 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
ctx := c.Request.Context()
ch, err := h.emailChannelSvc.GetByID(ctx, uint(emID))
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
if err != nil {
applogger.L().Errorf("Failed to get Email channel for update: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
@@ -222,7 +218,7 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
return
}
@@ -292,29 +288,30 @@ func (h *EmailChannelHandler) Update(c *gin.Context) {
updateReq := service.UpdateInboxRequest{
Name: *req.InboxName,
}
if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil {
if _, inboxErr := h.inboxSvc.Update(ctx, accountID, ch.InboxID, updateReq); inboxErr != nil {
applogger.L().Warnf("Failed to update Email inbox name: %v", inboxErr)
}
}
inbox, bindErr := h.inboxSvc.BindChannel(ctx, accountID, ch.InboxID, ch.ID, emailUpdateChannelConfig(req, ch))
if bindErr != nil {
applogger.L().Warnf("Failed to update Email inbox channel config: %v", bindErr)
c.JSON(http.StatusOK, h.serializeInboxForEmailChannel(c, ch))
return
}
c.JSON(http.StatusOK, gin.H{
"channel": ch,
"message": "Email channel updated successfully",
})
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// Delete removes an Email channel and its associated inbox.
// DELETE /api/v1/accounts/:id/channels/email_channel/:em_id
func (h *EmailChannelHandler) Delete(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
emIDStr := c.Param("em_id")
emID, err := strconv.ParseUint(emIDStr, 10, 64)
emID, err := parseUintParam(c, "em_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid em_id"})
return
@@ -322,20 +319,20 @@ func (h *EmailChannelHandler) Delete(c *gin.Context) {
ctx := c.Request.Context()
ch, err := h.emailChannelSvc.GetByID(ctx, uint(emID))
ch, err := h.emailChannelSvc.GetByID(ctx, emID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Email channel not found"})
return
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Email channel does not belong to this account"})
return
}
// Delete the channel record
if err := h.emailChannelSvc.Delete(ctx, uint(emID)); err != nil {
if err := h.emailChannelSvc.Delete(ctx, emID); err != nil {
applogger.L().Errorf("Failed to delete Email channel: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Email channel"})
return
@@ -343,34 +340,88 @@ func (h *EmailChannelHandler) Delete(c *gin.Context) {
// Delete the associated inbox
if ch.InboxID > 0 {
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
applogger.L().Warnf("Failed to delete inbox for Email channel: %v", delErr)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Email channel deleted successfully"})
c.Status(http.StatusOK)
}
// List lists all Email channels for an account.
// GET /api/v1/accounts/:id/channels/email_channel
func (h *EmailChannelHandler) List(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
ctx := c.Request.Context()
channels, err := h.emailChannelSvc.ListByAccount(ctx, uint(accountID))
channels, err := h.emailChannelSvc.ListByAccount(ctx, accountID)
if err != nil {
applogger.L().Errorf("Failed to list Email channels: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Email channels"})
return
}
c.JSON(http.StatusOK, gin.H{"channels": channels})
payload := make([]map[string]any, 0, len(channels))
for i := range channels {
payload = append(payload, h.serializeInboxForEmailChannel(c, &channels[i]))
}
c.JSON(http.StatusOK, gin.H{"payload": payload})
}
func (h *EmailChannelHandler) serializeInboxForEmailChannel(c *gin.Context, ch *channelmodel.ChannelEmail) map[string]any {
if ch != nil && ch.InboxID > 0 {
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
return serializeInbox(inbox)
}
}
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "email": ch.Email}
}
func emailChannelConfig(req CreateEmailChannelRequest) map[string]any {
return map[string]any{
"email": req.Email,
"mailbox_name": req.MailboxName,
"domain": extractDomain(req.Email),
"imap_enabled": req.IMAPEnabled,
"imap_address": req.IMAPAddress,
"imap_port": req.IMAPPort,
"imap_login": req.IMAPLogin,
"imap_password": req.IMAPPassword,
"imap_ssl_mode": req.IMAPSSLMode,
"imap_folder": req.IMAPFolder,
"smtp_enabled": req.SMTPEnabled,
"smtp_address": req.SMTPAddress,
"smtp_port": req.SMTPPort,
"smtp_login": req.SMTPLogin,
"smtp_password": req.SMTPPassword,
"smtp_ssl_mode": req.SMTPSSLMode,
}
}
func emailUpdateChannelConfig(_ UpdateEmailChannelRequest, ch *channelmodel.ChannelEmail) map[string]any {
return map[string]any{
"email": ch.Email,
"mailbox_name": ch.MailboxName,
"domain": ch.Domain,
"imap_enabled": ch.IMAPEnabled,
"imap_address": ch.IMAPAddress,
"imap_port": ch.IMAPPort,
"imap_login": ch.IMAPLogin,
"imap_password": ch.IMAPPassword,
"imap_ssl_mode": ch.IMAPSSLMode,
"imap_folder": ch.IMAPFolder,
"smtp_enabled": ch.SMTPEnabled,
"smtp_address": ch.SMTPAddress,
"smtp_port": ch.SMTPPort,
"smtp_login": ch.SMTPLogin,
"smtp_password": ch.SMTPPassword,
"smtp_ssl_mode": ch.SMTPSSLMode,
}
}
// extractDomain extracts the domain portion from an email address.
@@ -380,4 +431,4 @@ func extractDomain(email string) string {
return parts[1]
}
return ""
}
}
@@ -14,8 +14,8 @@ import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
channelmodel "github.com/gochat/gochat/internal/model/channel"
"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"
@@ -112,13 +112,16 @@ func TestEmailChannel_Create_Success(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "support@example.com", channelData["email"])
assert.Equal(t, "Support Email", resp["name"])
assert.Equal(t, "Channel::Email", resp["channel_type"])
assert.Equal(t, "support@example.com", resp["email"])
assert.Equal(t, "imap.example.com", resp["imap_address"])
require.NotContains(t, resp, "channel")
require.NotContains(t, resp, "inbox")
}
func TestEmailChannel_Create_InvalidAccountID(t *testing.T) {
@@ -161,14 +164,19 @@ func TestEmailChannel_Get_Success(t *testing.T) {
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelEmail{
Email: "get@example.com",
AccountID: accountID,
InboxID: 300,
IMAPEnabled: true,
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Get Email", "email", map[string]any{
"email": "get@example.com",
"imap_enabled": true,
})
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/email_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
@@ -177,9 +185,10 @@ func TestEmailChannel_Get_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "get@example.com", channelData["email"])
assert.Equal(t, "Get Email", resp["name"])
assert.Equal(t, "Channel::Email", resp["channel_type"])
assert.Equal(t, "get@example.com", resp["email"])
require.NotContains(t, resp, "channel")
}
func TestEmailChannel_Get_InvalidID(t *testing.T) {
@@ -216,14 +225,19 @@ func TestEmailChannel_Update_Success(t *testing.T) {
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelEmail{
Email: "update@example.com",
AccountID: accountID,
InboxID: 301,
IMAPEnabled: true,
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Update Email", "email", map[string]any{
"email": "update@example.com",
"imap_enabled": true,
})
ch.InboxID = inbox.ID
require.NoError(t, db.Save(ch).Error)
newEmail := "updated@example.com"
body := UpdateEmailChannelRequest{
@@ -239,9 +253,9 @@ func TestEmailChannel_Update_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "updated@example.com", channelData["email"])
assert.Equal(t, "Channel::Email", resp["channel_type"])
assert.Equal(t, "updated@example.com", resp["email"])
require.NotContains(t, resp, "channel")
}
func TestEmailChannel_Update_WrongAccount(t *testing.T) {
@@ -251,9 +265,9 @@ func TestEmailChannel_Update_WrongAccount(t *testing.T) {
// Seed a channel owned by a different account
ch := &channelmodel.ChannelEmail{
Email: "other@example.com",
AccountID: accountID + 999,
InboxID: 302,
Email: "other@example.com",
AccountID: accountID + 999,
InboxID: 302,
}
require.NoError(t, db.Create(ch).Error)
@@ -293,9 +307,9 @@ func TestEmailChannel_Delete_Success(t *testing.T) {
// Seed a channel
ch := &channelmodel.ChannelEmail{
Email: "delete@example.com",
AccountID: accountID,
InboxID: 303,
Email: "delete@example.com",
AccountID: accountID,
InboxID: 303,
}
require.NoError(t, db.Create(ch).Error)
@@ -318,9 +332,9 @@ func TestEmailChannel_Delete_WrongAccount(t *testing.T) {
// Seed a channel owned by a different account
ch := &channelmodel.ChannelEmail{
Email: "otherdel@example.com",
AccountID: accountID + 999,
InboxID: 304,
Email: "otherdel@example.com",
AccountID: accountID + 999,
InboxID: 304,
}
require.NoError(t, db.Create(ch).Error)
@@ -350,14 +364,18 @@ func TestEmailChannel_List_Success(t *testing.T) {
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed multiple channels
// Seed multiple channels and their associated inboxes.
for i := 0; i < 3; i++ {
ch := &channelmodel.ChannelEmail{
Email: "list" + strconv.Itoa(i) + "@example.com",
AccountID: accountID,
InboxID: uint(i + 400), // unique inbox IDs
Email: "list" + strconv.Itoa(i) + "@example.com",
AccountID: accountID,
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "Email List "+strconv.Itoa(i), "email", map[string]any{
"email": ch.Email,
})
ch.InboxID = inbox.ID
require.NoError(t, db.Save(ch).Error)
}
w := httptest.NewRecorder()
@@ -367,7 +385,7 @@ func TestEmailChannel_List_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 3)
}
@@ -395,7 +413,7 @@ func TestEmailChannel_List_Empty(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 0)
}
}
+92 -41
View File
@@ -14,7 +14,6 @@ package v1
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
@@ -56,19 +55,17 @@ func NewLINEChannelHandler(
// CreateLINEChannelRequest is the DTO for creating a LINE channel.
type CreateLINEChannelRequest struct {
ChannelID string `json:"channel_id" validate:"required"`
Name string `json:"name" validate:"required"`
ChannelID string `json:"channel_id" validate:"required"`
Name string `json:"name" validate:"required"`
ChannelAccessToken string `json:"channel_access_token" validate:"required"`
ChannelSecret string `json:"channel_secret" validate:"required"`
ChannelSecret string `json:"channel_secret" validate:"required"`
}
// Create adds a new LINE channel and creates the associated inbox.
// POST /api/v1/accounts/:id/channels/line_channel
func (h *LINEChannelHandler) Create(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid account_id: %v", err)
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
@@ -85,7 +82,7 @@ func (h *LINEChannelHandler) Create(c *gin.Context) {
}
ch := &channelmodel.ChannelLINE{
AccountID: uint(accountID),
AccountID: accountID,
ChannelID: req.ChannelID,
Name: req.Name,
}
@@ -101,8 +98,9 @@ func (h *LINEChannelHandler) Create(c *gin.Context) {
Name: req.Name,
ChannelType: "line",
Enabled: true,
Channel: lineCreateChannelConfig(req),
}
inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
inbox, err := h.inboxSvc.Create(c.Request.Context(), accountID, inboxReq)
if err != nil {
applogger.L().Errorf("Failed to create inbox for LINE channel: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to create inbox"})
@@ -113,11 +111,12 @@ inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
if err := h.lineChannelSvc.Update(c.Request.Context(), ch); err != nil {
applogger.L().Errorf("Failed to update LINE channel with inbox_id: %v", err)
}
inbox, err = h.inboxSvc.BindChannel(c.Request.Context(), accountID, inbox.ID, ch.ID, inboxReq.Channel)
if err != nil {
applogger.L().Warnf("Failed to bind LINE inbox channel config: %v", err)
}
c.JSON(http.StatusCreated, gin.H{
"channel": ch,
"inbox": inbox,
})
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// === Get ===
@@ -125,58 +124,56 @@ inbox, err := h.inboxSvc.Create(c.Request.Context(), uint(accountID), inboxReq)
// Get retrieves a LINE channel by ID.
// GET /api/v1/accounts/:id/channels/line_channel/:line_id
func (h *LINEChannelHandler) Get(c *gin.Context) {
lineIDStr := c.Param("line_id")
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
lineID, err := parseUintParam(c, "line_id")
if err != nil {
applogger.L().Errorf("Invalid line_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
return
}
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
if err != nil {
applogger.L().Errorf("Failed to get LINE channel: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
return
}
c.JSON(http.StatusOK, gin.H{"channel": ch})
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
}
// === Update ===
// UpdateLINEChannelRequest is the DTO for updating a LINE channel.
type UpdateLINEChannelRequest struct {
Name string `json:"name,omitempty"`
Name string `json:"name,omitempty"`
ChannelAccessToken string `json:"channel_access_token,omitempty"`
ChannelSecret string `json:"channel_secret,omitempty"`
ChannelSecret string `json:"channel_secret,omitempty"`
}
// Update modifies an existing LINE channel.
// PATCH /api/v1/accounts/:id/channels/line_channel/:line_id
func (h *LINEChannelHandler) Update(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
lineIDStr := c.Param("line_id")
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
lineID, err := parseUintParam(c, "line_id")
if err != nil {
applogger.L().Errorf("Invalid line_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
return
}
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
if err != nil {
applogger.L().Errorf("Failed to get LINE channel: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
return
}
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
return
}
@@ -197,8 +194,19 @@ func (h *LINEChannelHandler) Update(c *gin.Context) {
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to update LINE channel"})
return
}
if req.Name != "" {
if _, inboxErr := h.inboxSvc.Update(c.Request.Context(), accountID, ch.InboxID, service.UpdateInboxRequest{Name: req.Name}); inboxErr != nil {
applogger.L().Warnf("Failed to update LINE inbox name: %v", inboxErr)
}
}
inbox, bindErr := h.inboxSvc.BindChannel(c.Request.Context(), accountID, ch.InboxID, ch.ID, lineUpdateChannelConfig(req, ch))
if bindErr != nil {
applogger.L().Warnf("Failed to update LINE inbox channel config: %v", bindErr)
c.JSON(http.StatusOK, h.serializeInboxForLINEChannel(c, ch))
return
}
c.JSON(http.StatusOK, gin.H{"channel": ch})
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// === Delete ===
@@ -206,28 +214,27 @@ func (h *LINEChannelHandler) Update(c *gin.Context) {
// Delete removes a LINE channel and its associated inbox.
// DELETE /api/v1/accounts/:id/channels/line_channel/:line_id
func (h *LINEChannelHandler) Delete(c *gin.Context) {
accountID, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
lineIDStr := c.Param("line_id")
lineID, err := strconv.ParseUint(lineIDStr, 10, 64)
lineID, err := parseUintParam(c, "line_id")
if err != nil {
applogger.L().Errorf("Invalid line_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid line_id"})
return
}
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), uint(lineID))
ch, err := h.lineChannelSvc.GetByID(c.Request.Context(), lineID)
if err != nil {
applogger.L().Errorf("Failed to get LINE channel: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "LINE channel not found"})
return
}
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "channel does not belong to this account"})
return
}
@@ -239,13 +246,13 @@ func (h *LINEChannelHandler) Delete(c *gin.Context) {
}
}
if err := h.lineChannelSvc.Delete(c.Request.Context(), uint(lineID)); err != nil {
if err := h.lineChannelSvc.Delete(c.Request.Context(), lineID); err != nil {
applogger.L().Errorf("Failed to delete LINE channel: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete LINE channel"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "LINE channel deleted"})
c.Status(http.StatusOK)
}
// === List ===
@@ -253,10 +260,8 @@ func (h *LINEChannelHandler) Delete(c *gin.Context) {
// List retrieves all LINE channels for an account.
// GET /api/v1/accounts/:id/channels/line_channel
func (h *LINEChannelHandler) List(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid account_id: %v", err)
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
@@ -268,5 +273,51 @@ func (h *LINEChannelHandler) List(c *gin.Context) {
return
}
c.JSON(http.StatusOK, gin.H{"channels": channels, "account_id": uint(accountID)})
}
payload := make([]map[string]any, 0, len(channels))
for i := range channels {
if channels[i].AccountID == accountID {
payload = append(payload, h.serializeInboxForLINEChannel(c, &channels[i]))
}
}
c.JSON(http.StatusOK, gin.H{"payload": payload})
}
func (h *LINEChannelHandler) serializeInboxForLINEChannel(c *gin.Context, ch *channelmodel.ChannelLINE) map[string]any {
if ch != nil && ch.InboxID > 0 {
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
return serializeInbox(inbox)
}
}
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "channel_id": ch.ChannelID, "name": ch.Name}
}
func lineChannelConfigFromModel(ch *channelmodel.ChannelLINE) map[string]any {
return map[string]any{
"line_channel_id": ch.ChannelID,
"channel_id": ch.ChannelID,
}
}
func lineCreateChannelConfig(req CreateLINEChannelRequest) map[string]any {
return map[string]any{
"line_channel_id": req.ChannelID,
"line_channel_token": req.ChannelAccessToken,
"line_channel_secret": req.ChannelSecret,
"channel_id": req.ChannelID,
"channel_access_token": req.ChannelAccessToken,
"channel_secret": req.ChannelSecret,
}
}
func lineUpdateChannelConfig(req UpdateLINEChannelRequest, ch *channelmodel.ChannelLINE) map[string]any {
config := lineChannelConfigFromModel(ch)
if req.ChannelAccessToken != "" {
config["line_channel_token"] = req.ChannelAccessToken
config["channel_access_token"] = req.ChannelAccessToken
}
if req.ChannelSecret != "" {
config["line_channel_secret"] = req.ChannelSecret
config["channel_secret"] = req.ChannelSecret
}
return config
}
@@ -14,8 +14,8 @@ import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
channelmodel "github.com/gochat/gochat/internal/model/channel"
"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"
@@ -108,13 +108,17 @@ func TestLINEChannel_Create_Success(t *testing.T) {
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "line_chan_123", channelData["channel_id"])
assert.Equal(t, "LINE Official", resp["name"])
assert.Equal(t, "Channel::Line", resp["channel_type"])
assert.Equal(t, "line_chan_123", resp["line_channel_id"])
assert.Equal(t, "secret_value", resp["line_channel_secret"])
assert.Equal(t, "access_token_secret", resp["line_channel_token"])
require.NotContains(t, resp, "channel")
require.NotContains(t, resp, "inbox")
}
func TestLINEChannel_Create_InvalidAccountID(t *testing.T) {
@@ -160,14 +164,21 @@ func TestLINEChannel_Get_Success(t *testing.T) {
router := setupLINETestRouter(handler)
accountID := lineAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelLINE{
ChannelID: "line_get_123",
Name: "LINE Get Test",
AccountID: accountID,
InboxID: 700,
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "LINE Get Test", "line", map[string]any{
"line_channel_id": "line_get_123",
"line_channel_secret": "line-secret",
"line_channel_token": "line-token",
"channel_secret": "line-secret",
})
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/line_channel/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
@@ -176,9 +187,10 @@ func TestLINEChannel_Get_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "line_get_123", channelData["channel_id"])
assert.Equal(t, "LINE Get Test", resp["name"])
assert.Equal(t, "Channel::Line", resp["channel_type"])
assert.Equal(t, "line_get_123", resp["line_channel_id"])
require.NotContains(t, resp, "channel")
}
func TestLINEChannel_Get_InvalidID(t *testing.T) {
@@ -215,14 +227,21 @@ func TestLINEChannel_Update_Success(t *testing.T) {
router := setupLINETestRouter(handler)
accountID := lineAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelLINE{
ChannelID: "line_upd_123",
Name: "LINE Update Test",
AccountID: accountID,
InboxID: 701,
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, "LINE Update Test", "line", map[string]any{
"line_channel_id": "line_upd_123",
"line_channel_secret": "old-secret",
"line_channel_token": "old-token",
"channel_secret": "old-secret",
})
ch.InboxID = inbox.ID
require.NoError(t, db.Save(ch).Error)
body := UpdateLINEChannelRequest{
Name: "Updated LINE",
@@ -237,9 +256,10 @@ func TestLINEChannel_Update_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "Updated LINE", channelData["name"])
assert.Equal(t, "Updated LINE", resp["name"])
assert.Equal(t, "line_upd_123", resp["line_channel_id"])
assert.Equal(t, "old-secret", resp["line_channel_secret"])
require.NotContains(t, resp, "channel")
}
func TestLINEChannel_Update_WrongAccount(t *testing.T) {
@@ -346,15 +366,22 @@ func TestLINEChannel_List_Success(t *testing.T) {
router := setupLINETestRouter(handler)
accountID := lineAccountIDUint(db)
// Seed multiple channels
// Seed multiple channels and their associated inboxes.
for i := 0; i < 3; i++ {
ch := &channelmodel.ChannelLINE{
ChannelID: "line_list_" + strconv.Itoa(i),
Name: "LINE List " + strconv.Itoa(i),
AccountID: accountID,
InboxID: uint(i + 800), // unique inbox IDs
}
require.NoError(t, db.Create(ch).Error)
inbox := createTestChannelInbox(t, db, accountID, ch.ID, ch.Name, "line", map[string]any{
"line_channel_id": ch.ChannelID,
"line_channel_secret": "line-secret-" + strconv.Itoa(i),
"line_channel_token": "line-token-" + strconv.Itoa(i),
"channel_secret": "line-secret-" + strconv.Itoa(i),
})
ch.InboxID = inbox.ID
require.NoError(t, db.Save(ch).Error)
}
w := httptest.NewRecorder()
@@ -364,7 +391,7 @@ func TestLINEChannel_List_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 3)
}
@@ -392,7 +419,7 @@ func TestLINEChannel_List_Empty(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 0)
}
}
@@ -16,7 +16,6 @@ import (
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"github.com/gin-gonic/gin"
@@ -64,10 +63,8 @@ type CreateTwilioSMSChannelRequest struct {
// Create adds a new Twilio SMS channel and creates the associated inbox.
// POST /api/v1/accounts/:id/channels/twilio_channel
func (h *TwilioChannelHandler) Create(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
applogger.L().Errorf("Invalid account_id: %v", err)
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
@@ -92,7 +89,7 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
AccountSID: req.AccountSID,
PhoneNumber: phoneNumber,
MessagingServiceSID: req.MessagingServiceSID,
AccountID: uint(accountID),
AccountID: accountID,
}
if err := h.twChannelSvc.Create(ctx, ch); err != nil {
@@ -123,7 +120,7 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
},
}
inbox, err := h.inboxSvc.Create(ctx, uint(accountID), inboxReq)
inbox, err := h.inboxSvc.Create(ctx, accountID, inboxReq)
if err != nil {
applogger.L().Errorf("Failed to create inbox for Twilio SMS channel: %v", err)
// Rollback channel creation
@@ -139,6 +136,10 @@ func (h *TwilioChannelHandler) Create(c *gin.Context) {
if err := h.twChannelSvc.Update(ctx, ch); err != nil {
applogger.L().Errorf("Failed to update Twilio SMS channel with inbox_id: %v", err)
}
inbox, err = h.inboxSvc.BindChannel(ctx, accountID, inbox.ID, ch.ID, inboxReq.Channel)
if err != nil {
applogger.L().Warnf("Failed to bind Twilio inbox channel config: %v", err)
}
c.JSON(http.StatusOK, serializeInbox(inbox))
}
@@ -183,22 +184,21 @@ func bindTwilioCreateRequest(c *gin.Context) (CreateTwilioSMSChannelRequest, err
// Get retrieves a Twilio SMS channel by ID.
// GET /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Get(c *gin.Context) {
twIDStr := c.Param("tw_id")
twID, err := strconv.ParseUint(twIDStr, 10, 64)
twID, err := parseUintParam(c, "tw_id")
if err != nil {
applogger.L().Errorf("Invalid tw_id: %v", err)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
return
}
ch, err := h.twChannelSvc.GetByID(c.Request.Context(), uint(twID))
ch, err := h.twChannelSvc.GetByID(c.Request.Context(), twID)
if err != nil {
applogger.L().Errorf("Failed to get Twilio SMS channel: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
return
}
c.JSON(http.StatusOK, gin.H{"channel": ch})
c.JSON(http.StatusOK, h.serializeInboxForTwilioChannel(c, ch))
}
// UpdateTwilioSMSChannelRequest is the DTO for updating a Twilio SMS channel.
@@ -211,15 +211,13 @@ type UpdateTwilioSMSChannelRequest struct {
// Update updates a Twilio SMS channel configuration.
// PATCH /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Update(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
twIDStr := c.Param("tw_id")
twID, err := strconv.ParseUint(twIDStr, 10, 64)
twID, err := parseUintParam(c, "tw_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
return
@@ -227,7 +225,7 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
ctx := c.Request.Context()
ch, err := h.twChannelSvc.GetByID(ctx, uint(twID))
ch, err := h.twChannelSvc.GetByID(ctx, twID)
if err != nil {
applogger.L().Errorf("Failed to get Twilio SMS channel for update: %v", err)
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
@@ -235,7 +233,7 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"})
return
}
@@ -264,34 +262,30 @@ func (h *TwilioChannelHandler) Update(c *gin.Context) {
updateReq := service.UpdateInboxRequest{
Name: *req.InboxName,
}
if _, inboxErr := h.inboxSvc.Update(ctx, uint(accountID), ch.InboxID, updateReq); inboxErr != nil {
if _, inboxErr := h.inboxSvc.Update(ctx, accountID, ch.InboxID, updateReq); inboxErr != nil {
applogger.L().Warnf("Failed to update Twilio SMS inbox name: %v", inboxErr)
}
}
inbox, bindErr := h.inboxSvc.BindChannel(ctx, accountID, ch.InboxID, ch.ID, twilioChannelConfigFromModel(ch))
if bindErr != nil {
applogger.L().Warnf("Failed to update Twilio inbox channel config: %v", bindErr)
c.JSON(http.StatusOK, h.serializeInboxForTwilioChannel(c, ch))
return
}
c.JSON(http.StatusOK, gin.H{
"id": ch.ID,
"account_id": ch.AccountID,
"inbox_id": ch.InboxID,
"account_sid": ch.AccountSID,
"phone_number": ch.PhoneNumber,
"messaging_service_sid": ch.MessagingServiceSID,
"message": "Twilio SMS channel updated successfully",
})
c.JSON(http.StatusOK, serializeInbox(inbox))
}
// Delete removes a Twilio SMS channel and its associated inbox.
// DELETE /api/v1/accounts/:id/channels/twilio_channel/:tw_id
func (h *TwilioChannelHandler) Delete(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
twIDStr := c.Param("tw_id")
twID, err := strconv.ParseUint(twIDStr, 10, 64)
twID, err := parseUintParam(c, "tw_id")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid tw_id"})
return
@@ -299,20 +293,20 @@ func (h *TwilioChannelHandler) Delete(c *gin.Context) {
ctx := c.Request.Context()
ch, err := h.twChannelSvc.GetByID(ctx, uint(twID))
ch, err := h.twChannelSvc.GetByID(ctx, twID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Twilio SMS channel not found"})
return
}
// Verify account ownership
if ch.AccountID != uint(accountID) {
if ch.AccountID != accountID {
c.JSON(http.StatusForbidden, gin.H{"error": "Twilio SMS channel does not belong to this account"})
return
}
// Delete the channel record
if err := h.twChannelSvc.Delete(ctx, uint(twID)); err != nil {
if err := h.twChannelSvc.Delete(ctx, twID); err != nil {
applogger.L().Errorf("Failed to delete Twilio SMS channel: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to delete Twilio SMS channel"})
return
@@ -320,32 +314,57 @@ func (h *TwilioChannelHandler) Delete(c *gin.Context) {
// Delete the associated inbox
if ch.InboxID > 0 {
if delErr := h.inboxSvc.DeleteByAccount(ctx, uint(accountID), ch.InboxID); delErr != nil {
if delErr := h.inboxSvc.DeleteByAccount(ctx, accountID, ch.InboxID); delErr != nil {
applogger.L().Warnf("Failed to delete inbox for Twilio SMS channel: %v", delErr)
}
}
c.JSON(http.StatusOK, gin.H{"message": "Twilio SMS channel deleted successfully"})
c.Status(http.StatusOK)
}
// List lists all Twilio SMS channels for an account.
// GET /api/v1/accounts/:id/channels/twilio_channel
func (h *TwilioChannelHandler) List(c *gin.Context) {
accountIDStr := c.Param("id")
accountID, err := strconv.ParseUint(accountIDStr, 10, 64)
if err != nil {
accountID := parseAccountIDParam(c)
if accountID == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid account_id"})
return
}
ctx := c.Request.Context()
channels, err := h.twChannelSvc.ListByAccount(ctx, uint(accountID))
channels, err := h.twChannelSvc.ListByAccount(ctx, accountID)
if err != nil {
applogger.L().Errorf("Failed to list Twilio SMS channels: %v", err)
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": "failed to list Twilio SMS channels"})
return
}
c.JSON(http.StatusOK, gin.H{"channels": channels})
payload := make([]map[string]any, 0, len(channels))
for i := range channels {
payload = append(payload, h.serializeInboxForTwilioChannel(c, &channels[i]))
}
c.JSON(http.StatusOK, gin.H{"payload": payload})
}
func (h *TwilioChannelHandler) serializeInboxForTwilioChannel(c *gin.Context, ch *channelmodel.ChannelTwilioSMS) map[string]any {
if ch != nil && ch.InboxID > 0 {
if inbox, err := h.inboxSvc.GetByAccountAndID(c.Request.Context(), ch.AccountID, ch.InboxID); err == nil {
return serializeInbox(inbox)
}
}
return gin.H{"id": ch.ID, "account_id": ch.AccountID, "inbox_id": ch.InboxID, "account_sid": ch.AccountSID, "phone_number": ch.PhoneNumber, "messaging_service_sid": ch.MessagingServiceSID}
}
func twilioChannelConfigFromModel(ch *channelmodel.ChannelTwilioSMS) map[string]any {
medium := "sms"
if strings.HasPrefix(ch.PhoneNumber, "whatsapp:") {
medium = "whatsapp"
}
return map[string]any{
"account_sid": ch.AccountSID,
"phone_number": ch.PhoneNumber,
"messaging_service_sid": ch.MessagingServiceSID,
"medium": medium,
}
}
@@ -170,14 +170,20 @@ func TestTwilioChannel_Get_Success(t *testing.T) {
router := setupTwilioTestRouter(handler)
accountID := twilioAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelTwilioSMS{
AccountSID: "ACget123",
PhoneNumber: "+15559999999",
AccountID: accountID,
InboxID: 200,
}
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)
@@ -186,9 +192,10 @@ func TestTwilioChannel_Get_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channelData, ok := resp["channel"].(map[string]interface{})
require.True(t, ok)
assert.Equal(t, "ACget123", channelData["account_sid"])
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) {
@@ -225,14 +232,20 @@ func TestTwilioChannel_Update_Success(t *testing.T) {
router := setupTwilioTestRouter(handler)
accountID := twilioAccountIDUint(db)
// Seed a channel
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelTwilioSMS{
AccountSID: "ACupd123",
PhoneNumber: "+15558888888",
AccountID: accountID,
InboxID: 201,
}
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{
@@ -360,15 +373,21 @@ func TestTwilioChannel_List_Success(t *testing.T) {
router := setupTwilioTestRouter(handler)
accountID := twilioAccountIDUint(db)
// Seed multiple channels
// 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,
InboxID: uint(i + 100), // unique inbox IDs to satisfy uniqueIndex constraint
}
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()
@@ -378,7 +397,7 @@ func TestTwilioChannel_List_Success(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 3)
}
@@ -406,7 +425,7 @@ func TestTwilioChannel_List_Empty(t *testing.T) {
assert.Equal(t, http.StatusOK, w.Code)
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
channels, ok := resp["channels"].([]interface{})
channels, ok := resp["payload"].([]interface{})
require.True(t, ok)
assert.Len(t, channels, 0)
}
+23
View File
@@ -234,6 +234,29 @@ func (s *InboxService) Update(ctx context.Context, accountID, id uint, req Updat
return inbox, nil
}
// BindChannel persists the channel id and channel_config for channel-specific
// controllers that still create their dedicated channel record first.
func (s *InboxService) BindChannel(ctx context.Context, accountID, id, channelID uint, channel map[string]any) (*model.Inbox, error) {
inbox, err := s.repo.FindByAccountAndID(ctx, accountID, id)
if err != nil {
return nil, err
}
inbox.ChannelID = channelID
if len(channel) > 0 {
config := parseChannelConfigMap(inbox.ChannelConfig)
mergeInboxChannelConfig(config, channel)
configJSON, err := json.Marshal(config)
if err != nil {
return nil, err
}
inbox.ChannelConfig = string(configJSON)
}
if err := s.repo.Update(ctx, inbox); err != nil {
return nil, err
}
return inbox, nil
}
func applyCreateInboxSettings(inbox *model.Inbox, req CreateInboxRequest) {
if req.GreetingEnabled != nil {
inbox.GreetingEnabled = *req.GreetingEnabled