Files
gochat/internal/handler/api/v1/email_channel_handler_test.go
T

420 lines
14 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 setupEmailHandlerTest(t *testing.T) (*EmailChannelHandler, *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.ChannelEmail{},
))
t.Cleanup(func() {
sqlDB, _ := db.DB()
sqlDB.Close()
})
emailRepo := repository.NewChannelEmailRepo(db)
emailChannelSvc := service.NewChannelEmailService(emailRepo)
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 := NewEmailChannelHandler(emailChannelSvc, inboxSvc, emailRepo)
// Seed an account
account := &model.Account{Name: "EmailTestOrg", Locale: "en", Active: true}
require.NoError(t, db.Create(account).Error)
return handler, db
}
func emailAccountID(db *gorm.DB) string {
var account model.Account
db.First(&account)
return strconv.FormatUint(uint64(account.ID), 10)
}
func emailAccountIDUint(db *gorm.DB) uint {
var account model.Account
db.First(&account)
return account.ID
}
func setupEmailTestRouter(handler *EmailChannelHandler) *gin.Engine {
gin.SetMode(gin.TestMode)
r := gin.New()
ag := r.Group("/api/v1/accounts/:id")
ag.POST("/email_channels", handler.Create)
ag.GET("/email_channels", handler.List)
ig := ag.Group("/inboxes/:inbox_id/email_channels")
ig.GET("/:em_id", handler.Get)
ig.PATCH("/:em_id", handler.Update)
ig.DELETE("/:em_id", handler.Delete)
return r
}
// ── Create ──────────────────────────────────────────────────────
func TestEmailChannel_Create_Success(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
body := CreateEmailChannelRequest{
Email: "support@example.com",
IMAPEnabled: true,
IMAPAddress: "imap.example.com",
IMAPPort: 993,
IMAPLogin: "support@example.com",
IMAPPassword: "imapsecret",
IMAPSSLMode: "ssl",
InboxName: "Support Email",
}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+accountID+"/email_channels", 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 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) {
handler, _ := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
body := CreateEmailChannelRequest{
Email: "bad@example.com",
}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/invalid/email_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 TestEmailChannel_Create_InvalidRequestBody(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/api/v1/accounts/"+accountID+"/email_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 TestEmailChannel_Get_Success(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelEmail{
Email: "get@example.com",
AccountID: accountID,
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)
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 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) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/inboxes/1/email_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 em_id", resp["error"])
}
func TestEmailChannel_Get_NotFound(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/inboxes/1/email_channels/99999", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
// ── Update ──────────────────────────────────────────────────────
func TestEmailChannel_Update_Success(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel and its associated inbox.
ch := &channelmodel.ChannelEmail{
Email: "update@example.com",
AccountID: accountID,
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{
Email: &newEmail,
}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/email_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, "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) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel owned by a different account
ch := &channelmodel.ChannelEmail{
Email: "other@example.com",
AccountID: accountID + 999,
InboxID: 302,
}
require.NoError(t, db.Create(ch).Error)
newEmail := "hacked@example.com"
body := UpdateEmailChannelRequest{
Email: &newEmail,
}
b, _ := json.Marshal(body)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/inboxes/1/email_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 TestEmailChannel_Update_InvalidEmID(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPatch, "/api/v1/accounts/"+accountID+"/inboxes/1/email_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 TestEmailChannel_Delete_Success(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel
ch := &channelmodel.ChannelEmail{
Email: "delete@example.com",
AccountID: accountID,
InboxID: 303,
}
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/email_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.ChannelEmail{}).Where("id = ?", ch.ID).Count(&count)
assert.Equal(t, int64(0), count)
}
func TestEmailChannel_Delete_WrongAccount(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// Seed a channel owned by a different account
ch := &channelmodel.ChannelEmail{
Email: "otherdel@example.com",
AccountID: accountID + 999,
InboxID: 304,
}
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/email_channels/"+strconv.FormatUint(uint64(ch.ID), 10), nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestEmailChannel_Delete_NotFound(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodDelete, "/api/v1/accounts/"+accountID+"/inboxes/1/email_channels/99999", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
// ── List ──────────────────────────────────────────────────────
func TestEmailChannel_List_Success(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountIDUint(db)
// 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,
}
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()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+strconv.FormatUint(uint64(accountID), 10)+"/email_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 TestEmailChannel_List_InvalidAccountID(t *testing.T) {
handler, _ := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/invalid/email_channels", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestEmailChannel_List_Empty(t *testing.T) {
handler, db := setupEmailHandlerTest(t)
router := setupEmailTestRouter(handler)
accountID := emailAccountID(db)
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodGet, "/api/v1/accounts/"+accountID+"/email_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)
}