feat(widget): cover campaigns events and labels
This commit is contained in:
@@ -528,6 +528,84 @@ func (h *WidgetHandler) DestroyConversationCustomAttributes(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, conversation)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) ListInboxMembers(c *gin.Context) {
|
||||
websiteToken := c.Query("website_token")
|
||||
if websiteToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
|
||||
return
|
||||
}
|
||||
members, err := h.widgetService.GetInboxMembersByWebsiteToken(c.Request.Context(), websiteToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"payload": members})
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) ListCampaigns(c *gin.Context) {
|
||||
websiteToken := c.Query("website_token")
|
||||
if websiteToken == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "website_token is required"})
|
||||
return
|
||||
}
|
||||
campaigns, err := h.widgetService.GetCampaignsByWebsiteToken(c.Request.Context(), websiteToken)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, campaigns)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) CreateEvent(c *gin.Context) {
|
||||
websiteToken := c.Query("website_token")
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
EventInfo map[string]any `json:"event_info"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.widgetService.TrackEvent(c.Request.Context(), websiteToken, widgetTokenFromRequest(c), req.Name, req.EventInfo); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) AddLabel(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Label string `json:"label"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body", "details": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.widgetService.AddLabelToLatestConversation(c.Request.Context(), widgetToken, req.Label); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (h *WidgetHandler) RemoveLabel(c *gin.Context) {
|
||||
widgetToken := widgetTokenFromRequest(c)
|
||||
if widgetToken == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "widget_token required"})
|
||||
return
|
||||
}
|
||||
if err := h.widgetService.RemoveLabelFromLatestConversation(c.Request.Context(), widgetToken, c.Param("label_id")); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// SubmitOfflineMessage handles a visitor submitting a message when agents are offline.
|
||||
// POST /widget/offline_message
|
||||
// Reference: Chatwoot widget SDK — when business_hours are disabled and no agents online,
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"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"
|
||||
@@ -56,6 +57,11 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
&model.PreChatForm{},
|
||||
&model.WidgetFileUpload{},
|
||||
&model.WidgetOfflineMessage{},
|
||||
&model.User{},
|
||||
&model.AccountUser{},
|
||||
&model.InboxMember{},
|
||||
&model.Tag{},
|
||||
&campaign.Campaign{},
|
||||
))
|
||||
|
||||
t.Cleanup(func() {
|
||||
@@ -72,11 +78,14 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
preChatFormRepo := repository.NewPreChatFormRepo(db)
|
||||
fileUploadRepo := repository.NewWidgetFileUploadRepo(db)
|
||||
offlineMsgRepo := repository.NewWidgetOfflineMessageRepo(db)
|
||||
inboxMemberRepo := repository.NewInboxMemberRepo(db)
|
||||
tagRepo := repository.NewTagRepo(db)
|
||||
campaignRepo := repository.NewCampaignRepo(db)
|
||||
|
||||
widgetSvc := service.NewWidgetService(
|
||||
inboxRepo, contactRepo, contactInboxRepo, conversationRepo,
|
||||
messageRepo, &noopTypingIndicatorWidget{}, themeConfigRepo, preChatFormRepo,
|
||||
fileUploadRepo, offlineMsgRepo,
|
||||
fileUploadRepo, offlineMsgRepo, inboxMemberRepo, tagRepo, campaignRepo,
|
||||
)
|
||||
|
||||
handler := NewHandler(widgetSvc)
|
||||
@@ -101,6 +110,11 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
chatwootWidgetGroup.POST("/messages", handler.SendMessage)
|
||||
chatwootWidgetGroup.POST("/conversations", handler.CreateConversation)
|
||||
chatwootWidgetGroup.GET("/contact", handler.GetContact)
|
||||
chatwootWidgetGroup.GET("/campaigns", handler.ListCampaigns)
|
||||
chatwootWidgetGroup.GET("/inbox_members", handler.ListInboxMembers)
|
||||
chatwootWidgetGroup.POST("/events", handler.CreateEvent)
|
||||
chatwootWidgetGroup.POST("/labels", handler.AddLabel)
|
||||
chatwootWidgetGroup.DELETE("/labels/:label_id", handler.RemoveLabel)
|
||||
}
|
||||
|
||||
return db, router, handler
|
||||
@@ -278,6 +292,133 @@ func TestWidgetHandler_ChatwootMessages_AuthTokenAndNestedPayload(t *testing.T)
|
||||
require.Equal(t, http.StatusOK, wContact.Code)
|
||||
}
|
||||
|
||||
func TestWidgetHandler_ChatwootInboxMembers_Success(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, inbox := seedWidgetHandlerData(t, db)
|
||||
|
||||
user := &model.User{
|
||||
AccountID: inbox.AccountID,
|
||||
Name: "Ada Agent",
|
||||
Email: "ada@handler.test",
|
||||
Password: "secret",
|
||||
AvatarURL: "https://example.test/ada.png",
|
||||
Active: true,
|
||||
}
|
||||
require.NoError(t, db.Create(user).Error)
|
||||
require.NoError(t, db.Create(&model.InboxMember{
|
||||
InboxID: inbox.ID,
|
||||
UserID: user.ID,
|
||||
Role: "agent",
|
||||
AvailabilityStatus: "online",
|
||||
}).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/widget/inbox_members?website_token=handler_ws_token_123", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
payload := resp["payload"].([]interface{})
|
||||
require.Len(t, payload, 1)
|
||||
member := payload[0].(map[string]interface{})
|
||||
assert.Equal(t, "Ada Agent", member["name"])
|
||||
assert.Equal(t, "online", member["availability_status"])
|
||||
}
|
||||
|
||||
func TestWidgetHandler_ChatwootCampaigns_Success(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, inbox := seedWidgetHandlerData(t, db)
|
||||
|
||||
require.NoError(t, db.Create(&campaign.Campaign{
|
||||
AccountID: inbox.AccountID,
|
||||
InboxID: inbox.ID,
|
||||
DisplayID: 42,
|
||||
Title: "Welcome",
|
||||
Message: "Welcome to support",
|
||||
CampaignType: campaign.CampaignTypeOngoing,
|
||||
TriggerRules: `{"url":"https://example.test","time_on_page":10}`,
|
||||
Enabled: true,
|
||||
}).Error)
|
||||
disabledCampaign := &campaign.Campaign{
|
||||
AccountID: inbox.AccountID,
|
||||
InboxID: inbox.ID,
|
||||
DisplayID: 43,
|
||||
Title: "Disabled",
|
||||
Message: "Hidden",
|
||||
CampaignType: campaign.CampaignTypeOngoing,
|
||||
Enabled: false,
|
||||
}
|
||||
require.NoError(t, db.Create(disabledCampaign).Error)
|
||||
require.NoError(t, db.Model(disabledCampaign).Update("enabled", false).Error)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/api/v1/widget/campaigns?website_token=handler_ws_token_123", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
require.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
var resp []map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
require.Len(t, resp, 1)
|
||||
assert.Equal(t, float64(42), resp[0]["id"])
|
||||
assert.Equal(t, "Welcome to support", resp[0]["message"])
|
||||
rules := resp[0]["trigger_rules"].(map[string]interface{})
|
||||
assert.Equal(t, "https://example.test", rules["url"])
|
||||
}
|
||||
|
||||
func TestWidgetHandler_ChatwootEventsAndLabels(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, inbox := seedWidgetHandlerData(t, db)
|
||||
require.NoError(t, db.Create(&model.Tag{AccountID: inbox.AccountID, Name: "vip"}).Error)
|
||||
|
||||
wConfig := httptest.NewRecorder()
|
||||
reqConfig, _ := http.NewRequest("POST", "/api/v1/widget/config?website_token=handler_ws_token_123", nil)
|
||||
router.ServeHTTP(wConfig, reqConfig)
|
||||
require.Equal(t, http.StatusOK, wConfig.Code)
|
||||
|
||||
var configResp map[string]interface{}
|
||||
require.NoError(t, json.Unmarshal(wConfig.Body.Bytes(), &configResp))
|
||||
authToken := configResp["contact"].(map[string]interface{})["pubsub_token"].(string)
|
||||
|
||||
body := map[string]interface{}{"message": map[string]interface{}{"content": "Label me"}}
|
||||
bodyJSON, _ := json.Marshal(body)
|
||||
wMessage := httptest.NewRecorder()
|
||||
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages", bytes.NewReader(bodyJSON))
|
||||
reqMessage.Header.Set("Content-Type", "application/json")
|
||||
reqMessage.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wMessage, reqMessage)
|
||||
require.Equal(t, http.StatusOK, wMessage.Code)
|
||||
|
||||
wEvent := httptest.NewRecorder()
|
||||
eventBody, _ := json.Marshal(map[string]interface{}{"name": "widget.opened", "event_info": map[string]interface{}{"source": "test"}})
|
||||
reqEvent, _ := http.NewRequest("POST", "/api/v1/widget/events?website_token=handler_ws_token_123", bytes.NewReader(eventBody))
|
||||
reqEvent.Header.Set("Content-Type", "application/json")
|
||||
reqEvent.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wEvent, reqEvent)
|
||||
require.Equal(t, http.StatusNoContent, wEvent.Code)
|
||||
|
||||
wAdd := httptest.NewRecorder()
|
||||
labelBody, _ := json.Marshal(map[string]interface{}{"label": "vip"})
|
||||
reqAdd, _ := http.NewRequest("POST", "/api/v1/widget/labels", bytes.NewReader(labelBody))
|
||||
reqAdd.Header.Set("Content-Type", "application/json")
|
||||
reqAdd.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wAdd, reqAdd)
|
||||
require.Equal(t, http.StatusNoContent, wAdd.Code)
|
||||
|
||||
var conversation model.Conversation
|
||||
require.NoError(t, db.Where("inbox_id = ?", inbox.ID).First(&conversation).Error)
|
||||
assert.Equal(t, "vip", conversation.Labels)
|
||||
|
||||
wRemove := httptest.NewRecorder()
|
||||
reqRemove, _ := http.NewRequest("DELETE", "/api/v1/widget/labels/vip", nil)
|
||||
reqRemove.Header.Set("X-Auth-Token", authToken)
|
||||
router.ServeHTTP(wRemove, reqRemove)
|
||||
require.Equal(t, http.StatusNoContent, wRemove.Code)
|
||||
|
||||
require.NoError(t, db.First(&conversation, conversation.ID).Error)
|
||||
assert.Empty(t, conversation.Labels)
|
||||
}
|
||||
|
||||
// ========== SendMessage Handler Tests ==========
|
||||
|
||||
func TestWidgetHandler_SendMessage_Success(t *testing.T) {
|
||||
|
||||
@@ -87,6 +87,9 @@ func (s *WidgetThemeHandlerTestSuite) SetupSuite() {
|
||||
preChatFormRepo,
|
||||
fileUploadRepo,
|
||||
offlineMessageRepo,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
s.handler = NewHandler(widgetSvc)
|
||||
@@ -364,4 +367,4 @@ func (s *WidgetThemeHandlerTestSuite) TestGetFileUploadStatus_InvalidUploadUUID(
|
||||
|
||||
func TestWidgetThemeHandlerTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(WidgetThemeHandlerTestSuite))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user