feat(public): implement chatwoot public inbox flow
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/gochat/gochat/internal/campaign"
|
||||
"github.com/gochat/gochat/internal/model"
|
||||
channelmodel "github.com/gochat/gochat/internal/model/channel"
|
||||
"github.com/gochat/gochat/internal/repository"
|
||||
"github.com/gochat/gochat/internal/service"
|
||||
ws "github.com/gochat/gochat/internal/ws"
|
||||
@@ -62,6 +63,7 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
&model.AccountUser{},
|
||||
&model.InboxMember{},
|
||||
&model.Tag{},
|
||||
&channelmodel.ChannelAPI{},
|
||||
&campaign.Campaign{},
|
||||
))
|
||||
|
||||
@@ -122,6 +124,34 @@ func setupWidgetHandlerTest(t *testing.T) (*gorm.DB, *gin.Engine, *WidgetHandler
|
||||
chatwootWidgetGroup.POST("/integrations/dyte/add_participant_to_meeting", handler.AddDyteParticipantToMeeting)
|
||||
}
|
||||
|
||||
publicInboxes := router.Group("/public/api/v1/inboxes")
|
||||
{
|
||||
publicInboxes.GET("/:inbox_id", handler.PublicInboxShow)
|
||||
contacts := publicInboxes.Group("/:inbox_id/contacts")
|
||||
{
|
||||
contacts.POST("", handler.PublicCreateContact)
|
||||
contacts.GET("/:contact_id", handler.PublicGetContact)
|
||||
contacts.PUT("/:contact_id", handler.PublicUpdateContact)
|
||||
contacts.PATCH("/:contact_id", handler.PublicUpdateContact)
|
||||
conversations := contacts.Group("/:contact_id/conversations")
|
||||
{
|
||||
conversations.GET("", handler.PublicListConversations)
|
||||
conversations.POST("", handler.PublicCreateConversation)
|
||||
conversations.GET("/:conversation_id", handler.PublicGetConversation)
|
||||
conversations.POST("/:conversation_id/toggle_status", handler.PublicToggleStatus)
|
||||
conversations.POST("/:conversation_id/toggle_typing", handler.PublicToggleTyping)
|
||||
conversations.POST("/:conversation_id/update_last_seen", handler.PublicUpdateLastSeen)
|
||||
messages := conversations.Group("/:conversation_id/messages")
|
||||
{
|
||||
messages.GET("", handler.PublicListMessages)
|
||||
messages.POST("", handler.PublicCreateMessage)
|
||||
messages.PUT("/:message_id", handler.PublicUpdateMessage)
|
||||
messages.PATCH("/:message_id", handler.PublicUpdateMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return db, router, handler
|
||||
}
|
||||
|
||||
@@ -152,6 +182,32 @@ func seedWidgetHandlerData(t *testing.T, db *gorm.DB) (*model.Account, *model.In
|
||||
return account, inbox
|
||||
}
|
||||
|
||||
func seedPublicAPIInbox(t *testing.T, db *gorm.DB) (*model.Account, *model.Inbox, *channelmodel.ChannelAPI) {
|
||||
t.Helper()
|
||||
|
||||
account := &model.Account{Name: "PublicAPIOrg", Locale: "en", Status: "active"}
|
||||
require.NoError(t, db.Create(account).Error)
|
||||
|
||||
inbox := &model.Inbox{
|
||||
AccountID: account.ID,
|
||||
Name: "Public API Inbox",
|
||||
ChannelType: "api",
|
||||
ChannelID: 1,
|
||||
Enabled: true,
|
||||
Timezone: "UTC",
|
||||
GreetingEnabled: true,
|
||||
CsatSurveyEnabled: true,
|
||||
WorkingHoursEnabled: false,
|
||||
AllowMessagesAfterResolved: true,
|
||||
}
|
||||
require.NoError(t, db.Create(inbox).Error)
|
||||
|
||||
channelAPI := &channelmodel.ChannelAPI{InboxID: inbox.ID, Identifier: "public-api-inbox", HMACToken: "public_hmac_secret"}
|
||||
require.NoError(t, db.Create(channelAPI).Error)
|
||||
|
||||
return account, inbox, channelAPI
|
||||
}
|
||||
|
||||
// ========== Init Handler Tests ==========
|
||||
|
||||
func TestWidgetHandler_Init_Success(t *testing.T) {
|
||||
@@ -1053,3 +1109,105 @@ func TestWidgetHandler_Init_HMACInvalid(t *testing.T) {
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||
assert.NotEmpty(t, resp["widget_token"])
|
||||
}
|
||||
|
||||
func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T) {
|
||||
db, router, _ := setupWidgetHandlerTest(t)
|
||||
_, inbox, _ := seedPublicAPIInbox(t, db)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/public-api-inbox", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var inboxResp map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &inboxResp))
|
||||
assert.Equal(t, "public-api-inbox", inboxResp["identifier"])
|
||||
assert.Equal(t, inbox.Name, inboxResp["name"])
|
||||
assert.Equal(t, true, inboxResp["identity_validation_enabled"])
|
||||
|
||||
contactBody := map[string]any{
|
||||
"source_id": "public-source-1",
|
||||
"name": "Public Visitor",
|
||||
"email": "Visitor@Public.test",
|
||||
"phone_number": "+15550001111",
|
||||
"custom_attributes": map[string]any{
|
||||
"plan": "trial",
|
||||
},
|
||||
}
|
||||
contactJSON, _ := json.Marshal(contactBody)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts", bytes.NewReader(contactJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var contactResp map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &contactResp))
|
||||
assert.Equal(t, "public-source-1", contactResp["source_id"])
|
||||
assert.Equal(t, "Public Visitor", contactResp["name"])
|
||||
assert.Equal(t, "visitor@public.test", contactResp["email"])
|
||||
assert.NotEmpty(t, contactResp["pubsub_token"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations", bytes.NewReader([]byte(`{"custom_attributes":{"topic":"sales"}}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var conversationResp map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &conversationResp))
|
||||
conversationID := strconv.FormatUint(uint64(conversationResp["id"].(float64)), 10)
|
||||
assert.Equal(t, "open", conversationResp["status"])
|
||||
|
||||
messageBody := map[string]any{"content": "Hello from public API", "echo_id": "echo-1"}
|
||||
messageJSON, _ := json.Marshal(messageBody)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/messages", bytes.NewReader(messageJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var messageResp map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &messageResp))
|
||||
messageID := strconv.FormatUint(uint64(messageResp["id"].(float64)), 10)
|
||||
assert.Equal(t, "Hello from public API", messageResp["content"])
|
||||
assert.Equal(t, "incoming", messageResp["message_type"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/messages", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
var messagesResp []map[string]any
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &messagesResp))
|
||||
require.Len(t, messagesResp, 1)
|
||||
assert.Equal(t, "Hello from public API", messagesResp[0]["content"])
|
||||
|
||||
updateBody := map[string]any{"submitted_values": []map[string]any{{"name": "email", "value": "visitor@public.test"}}}
|
||||
updateJSON, _ := json.Marshal(updateBody)
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PATCH", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/messages/"+messageID, bytes.NewReader(updateJSON))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &messageResp))
|
||||
attrs := messageResp["content_attributes"].(map[string]any)
|
||||
assert.NotEmpty(t, attrs["submitted_values"])
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/toggle_status", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/update_last_seen", nil)
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("POST", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/toggle_typing", bytes.NewReader([]byte(`{"typing_status":"on"}`)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
router.ServeHTTP(w, req)
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user