Align GoChat with Chatwoot frontend contracts

This commit is contained in:
2026-06-13 22:13:32 +08:00
parent 71abf58636
commit d884fdda0a
162 changed files with 9825 additions and 528 deletions
@@ -43,6 +43,20 @@ func (n *noopTypingIndicatorWidget) SetTypingOff(_ context.Context, _ uint, _ ui
return nil
}
type recordingWidgetEventPublisher struct {
accountID uint
pubsubToken string
eventType string
payload interface{}
}
func (p *recordingWidgetEventPublisher) PublishWidgetEvent(accountID uint, pubsubToken string, eventType string, payload interface{}) {
p.accountID = accountID
p.pubsubToken = pubsubToken
p.eventType = eventType
p.payload = payload
}
type recordingWidgetTranscriptDeliverer struct {
requests []automation.AutomationTranscriptRequest
err error
@@ -332,13 +346,61 @@ func TestWidgetHandler_ChatwootConfig_Success(t *testing.T) {
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
config := resp["website_channel_config"].(map[string]interface{})
assertWidgetConfigFixtureShape(t, config)
assert.NotEmpty(t, config["auth_token"])
assert.Equal(t, "handler_ws_token_123", config["website_token"])
assert.Equal(t, "Handler Widget Inbox", config["website_name"])
contact := resp["contact"].(map[string]interface{})
assertWidgetConfigContactFixtureShape(t, contact)
assert.NotEmpty(t, contact["id"])
assert.NotEmpty(t, contact["pubsub_token"])
globalConfig := resp["global_config"].(map[string]interface{})
assert.Contains(t, globalConfig, "directUploadsEnabled")
assert.Contains(t, globalConfig, "maximumFileUploadSize")
}
func TestWidgetHandler_ChatwootConfigPreChatFormOptionsMatchFrontendMixin(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, inbox := seedWidgetHandlerData(t, db)
channelConfig := map[string]interface{}{
"website_token": "handler_ws_token_123",
"hmac_token": "handler_hmac_secret",
"widget_color": "#1f93ff",
"welcome_title": "Hi",
"welcome_tagline": "We reply fast",
"pre_chat_fields_enabled": true,
"pre_chat_message": "Tell us about yourself",
"selected_feature_flags": []string{"attachments", "emoji_picker", "end_conversation"},
"continuity_via_email": true,
"offline_message_enabled": true,
"offline_message_title": "We are away",
"offline_message_description": "Leave a message",
}
configJSON, err := json.Marshal(channelConfig)
require.NoError(t, err)
require.NoError(t, db.Model(inbox).Update("channel_config", string(configJSON)).Error)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/config?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))
config := resp["website_channel_config"].(map[string]interface{})
assert.True(t, config["preChatFormEnabled"].(bool))
options := config["preChatFormOptions"].(map[string]interface{})
assert.Equal(t, "Tell us about yourself", options["pre_chat_message"])
fields := options["pre_chat_fields"].([]interface{})
require.NotEmpty(t, fields)
firstField := fields[0].(map[string]interface{})
for _, key := range []string{"label", "name", "type", "field_type", "required", "enabled"} {
assert.Contains(t, firstField, key)
}
assert.Equal(t, true, firstField["enabled"])
assert.Contains(t, config["enabledFeatures"], "attachments")
}
func TestWidgetHandler_ChatwootConfig_InvalidWebsiteTokenReturnsNotFound(t *testing.T) {
@@ -389,6 +451,144 @@ func TestWidgetHandler_ChatwootConfig_InvalidAuthTokenCreatesNewContact(t *testi
assert.Contains(t, resp, "global_config")
}
func TestWidgetHandler_ChatwootConversationQueryTokenReusesSession(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, inbox := seedWidgetHandlerData(t, db)
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)
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "Popout session message"}})
wMessage := httptest.NewRecorder()
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMessage.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMessage, reqMessage)
require.Equal(t, http.StatusOK, wMessage.Code, wMessage.Body.String())
var messageResp map[string]interface{}
require.NoError(t, json.Unmarshal(wMessage.Body.Bytes(), &messageResp))
assert.Equal(t, "Popout session message", messageResp["content"])
assert.Equal(t, "incoming", messageResp["message_type"])
assertWidgetMessageFixtureShape(t, messageResp)
wLatest := httptest.NewRecorder()
reqLatest, _ := http.NewRequest("GET", "/api/v1/widget/messages?cw_conversation="+authToken, nil)
router.ServeHTTP(wLatest, reqLatest)
require.Equal(t, http.StatusOK, wLatest.Code, wLatest.Body.String())
var latestResp map[string]interface{}
require.NoError(t, json.Unmarshal(wLatest.Body.Bytes(), &latestResp))
payload := latestResp["payload"].([]interface{})
require.Len(t, payload, 1)
assert.Equal(t, "Popout session message", payload[0].(map[string]interface{})["content"])
var conversation model.Conversation
require.NoError(t, db.Where("inbox_id = ?", inbox.ID).First(&conversation).Error)
assert.Equal(t, string(model.ConversationStatusOpen), conversation.Status)
}
func TestWidgetHandler_ChatwootConfigCwConversationReusesContactInbox(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, inbox := seedWidgetHandlerData(t, db)
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)
contactID := uint(configResp["contact"].(map[string]interface{})["id"].(float64))
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "Popout boot history"}})
wMessage := httptest.NewRecorder()
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMessage.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMessage, reqMessage)
require.Equal(t, http.StatusOK, wMessage.Code, wMessage.Body.String())
var beforeContacts int64
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ?", inbox.AccountID).Count(&beforeContacts).Error)
wPopoutConfig := httptest.NewRecorder()
reqPopoutConfig, _ := http.NewRequest("POST", "/api/v1/widget/config?website_token=handler_ws_token_123&cw_conversation="+authToken, nil)
router.ServeHTTP(wPopoutConfig, reqPopoutConfig)
require.Equal(t, http.StatusOK, wPopoutConfig.Code, wPopoutConfig.Body.String())
var popoutConfigResp map[string]interface{}
require.NoError(t, json.Unmarshal(wPopoutConfig.Body.Bytes(), &popoutConfigResp))
popoutContact := popoutConfigResp["contact"].(map[string]interface{})
assert.Equal(t, float64(contactID), popoutContact["id"])
assert.Equal(t, authToken, popoutContact["pubsub_token"])
assertWidgetConfigContactFixtureShape(t, popoutContact)
var afterContacts int64
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ?", inbox.AccountID).Count(&afterContacts).Error)
assert.Equal(t, beforeContacts, afterContacts)
wLatest := httptest.NewRecorder()
reqLatest, _ := http.NewRequest("GET", "/api/v1/widget/messages?cw_conversation="+authToken, nil)
router.ServeHTTP(wLatest, reqLatest)
require.Equal(t, http.StatusOK, wLatest.Code, wLatest.Body.String())
var latestResp map[string]interface{}
require.NoError(t, json.Unmarshal(wLatest.Body.Bytes(), &latestResp))
payload := latestResp["payload"].([]interface{})
require.Len(t, payload, 1)
assert.Equal(t, "Popout boot history", payload[0].(map[string]interface{})["content"])
}
func TestWidgetHandler_ChatwootConversationCookieReusesSession(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, inbox := seedWidgetHandlerData(t, db)
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)
contactID := uint(configResp["contact"].(map[string]interface{})["id"].(float64))
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "Cookie persisted history"}})
wMessage := httptest.NewRecorder()
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages", bytes.NewReader(messageBody))
reqMessage.Header.Set("Content-Type", "application/json")
reqMessage.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wMessage, reqMessage)
require.Equal(t, http.StatusOK, wMessage.Code, wMessage.Body.String())
var beforeContacts int64
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ?", inbox.AccountID).Count(&beforeContacts).Error)
wCookieConfig := httptest.NewRecorder()
reqCookieConfig, _ := http.NewRequest("POST", "/api/v1/widget/config?website_token=handler_ws_token_123", nil)
reqCookieConfig.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wCookieConfig, reqCookieConfig)
require.Equal(t, http.StatusOK, wCookieConfig.Code, wCookieConfig.Body.String())
var cookieConfigResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCookieConfig.Body.Bytes(), &cookieConfigResp))
cookieContact := cookieConfigResp["contact"].(map[string]interface{})
assert.Equal(t, float64(contactID), cookieContact["id"])
assert.Equal(t, authToken, cookieContact["pubsub_token"])
var afterContacts int64
require.NoError(t, db.Model(&model.Contact{}).Where("account_id = ?", inbox.AccountID).Count(&afterContacts).Error)
assert.Equal(t, beforeContacts, afterContacts)
wLatest := httptest.NewRecorder()
reqLatest, _ := http.NewRequest("GET", "/api/v1/widget/messages", nil)
reqLatest.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wLatest, reqLatest)
require.Equal(t, http.StatusOK, wLatest.Code, wLatest.Body.String())
var latestResp map[string]interface{}
require.NoError(t, json.Unmarshal(wLatest.Body.Bytes(), &latestResp))
payload := latestResp["payload"].([]interface{})
require.Len(t, payload, 1)
assert.Equal(t, "Cookie persisted history", payload[0].(map[string]interface{})["content"])
}
func TestWidgetHandler_ChatwootMessageAppliesAttrsAndLabelsToNewConversation(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
account, _ := seedWidgetHandlerData(t, db)
@@ -527,6 +727,7 @@ func TestWidgetHandler_ChatwootMessages_AuthTokenAndNestedPayload(t *testing.T)
var messageResp map[string]interface{}
require.NoError(t, json.Unmarshal(wMessage.Body.Bytes(), &messageResp))
assertWidgetMessageFixtureShape(t, messageResp)
assert.Nil(t, messageResp["message"])
assert.Equal(t, "Hello from Chatwoot widget", messageResp["content"])
assert.NotEmpty(t, messageResp["conversation_id"])
@@ -542,6 +743,7 @@ func TestWidgetHandler_ChatwootMessages_AuthTokenAndNestedPayload(t *testing.T)
payload := indexResp["payload"].([]interface{})
require.Len(t, payload, 1)
firstMessage := payload[0].(map[string]interface{})
assertWidgetMessageFixtureShape(t, firstMessage)
assert.Equal(t, "Hello from Chatwoot widget", firstMessage["content"])
wContact := httptest.NewRecorder()
@@ -549,6 +751,45 @@ func TestWidgetHandler_ChatwootMessages_AuthTokenAndNestedPayload(t *testing.T)
reqContact.Header.Set("X-Auth-Token", authToken)
router.ServeHTTP(wContact, reqContact)
require.Equal(t, http.StatusOK, wContact.Code)
var contactResp map[string]interface{}
require.NoError(t, json.Unmarshal(wContact.Body.Bytes(), &contactResp))
assertWidgetContactFixtureShape(t, contactResp)
}
func TestWidgetHandler_ChatwootMessagePublishesWidgetRealtimePayload(t *testing.T) {
db, router, handler := setupWidgetHandlerTest(t)
_, _ = seedWidgetHandlerData(t, db)
publisher := &recordingWidgetEventPublisher{}
handler.WithEventPublisher(publisher)
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, _ := json.Marshal(map[string]interface{}{
"message": map[string]interface{}{"content": "Realtime widget message"},
})
wMessage := httptest.NewRecorder()
reqMessage, _ := http.NewRequest("POST", "/api/v1/widget/messages", bytes.NewReader(body))
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)
assert.Equal(t, uint(1), publisher.accountID)
assert.Equal(t, authToken, publisher.pubsubToken)
assert.Equal(t, ws.EventMessageCreated, publisher.eventType)
payload, ok := publisher.payload.(gin.H)
require.True(t, ok, "expected widget realtime payload to use Chatwoot widget message shape")
assert.Equal(t, "Realtime widget message", payload["content"])
assert.Equal(t, uint(1), payload["account_id"])
assert.NotEmpty(t, payload["conversation_id"])
assertWidgetMessageFixtureShape(t, payload)
}
func TestWidgetHandler_ChatwootMessagesIndexFiltersInternalMessages(t *testing.T) {
@@ -717,10 +958,12 @@ func TestWidgetHandler_ChatwootMessageDirectUploadAttachment(t *testing.T) {
var messageResp map[string]interface{}
require.NoError(t, json.Unmarshal(wMessage.Body.Bytes(), &messageResp))
assertWidgetMessageFixtureShape(t, messageResp)
assert.Empty(t, messageResp["content"])
attachments := messageResp["attachments"].([]interface{})
require.Len(t, attachments, 1)
attachmentPayload := attachments[0].(map[string]interface{})
assertWidgetAttachmentFixtureShape(t, attachmentPayload)
assert.Equal(t, "/uploads/widget_direct/signed-widget-upload-1.png", attachmentPayload["data_url"])
assert.Equal(t, "image", attachmentPayload["file_type"])
assert.Equal(t, float64(account.ID), attachmentPayload["account_id"])
@@ -743,9 +986,11 @@ func TestWidgetHandler_ChatwootMessageDirectUploadAttachment(t *testing.T) {
payload := indexResp["payload"].([]interface{})
require.Len(t, payload, 1)
indexedMessage := payload[0].(map[string]interface{})
assertWidgetMessageFixtureShape(t, indexedMessage)
indexedAttachments := indexedMessage["attachments"].([]interface{})
require.Len(t, indexedAttachments, 1)
indexedAttachment := indexedAttachments[0].(map[string]interface{})
assertWidgetAttachmentFixtureShape(t, indexedAttachment)
assert.Equal(t, "/uploads/widget_direct/signed-widget-upload-1.png", indexedAttachment["data_url"])
assert.Equal(t, float64(account.ID), indexedAttachment["account_id"])
assert.NotContains(t, indexedAttachment, "created_at")
@@ -1463,6 +1708,7 @@ func TestWidgetHandler_ChatwootCampaigns_Success(t *testing.T) {
var resp []map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
require.Len(t, resp, 1)
assertWidgetCampaignFixtureShape(t, resp[0])
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{})
@@ -1530,6 +1776,14 @@ func TestWidgetHandler_ChatwootEventsAndLabels(t *testing.T) {
router.ServeHTTP(wEvent, reqEvent)
require.Equal(t, http.StatusNoContent, wEvent.Code)
wCookieEvent := httptest.NewRecorder()
cookieEventBody, _ := json.Marshal(map[string]interface{}{"name": "webwidget.triggered", "event_info": map[string]interface{}{"source": "cookie"}})
reqCookieEvent, _ := http.NewRequest("POST", "/api/v1/widget/events?website_token=handler_ws_token_123", bytes.NewReader(cookieEventBody))
reqCookieEvent.Header.Set("Content-Type", "application/json")
reqCookieEvent.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wCookieEvent, reqCookieEvent)
require.Equal(t, http.StatusNoContent, wCookieEvent.Code)
wAdd := httptest.NewRecorder()
labelBody, _ := json.Marshal(map[string]interface{}{"label": "vip"})
reqAdd, _ := http.NewRequest("POST", "/api/v1/widget/labels", bytes.NewReader(labelBody))
@@ -1549,6 +1803,25 @@ func TestWidgetHandler_ChatwootEventsAndLabels(t *testing.T) {
require.NoError(t, db.First(&conversation, conversation.ID).Error)
assert.Empty(t, conversation.Labels)
wCookieAdd := httptest.NewRecorder()
reqCookieAdd, _ := http.NewRequest("POST", "/api/v1/widget/labels", bytes.NewReader(labelBody))
reqCookieAdd.Header.Set("Content-Type", "application/json")
reqCookieAdd.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wCookieAdd, reqCookieAdd)
require.Equal(t, http.StatusNoContent, wCookieAdd.Code)
require.NoError(t, db.First(&conversation, conversation.ID).Error)
assert.Equal(t, "vip", conversation.Labels)
wCookieRemove := httptest.NewRecorder()
reqCookieRemove, _ := http.NewRequest("DELETE", "/api/v1/widget/labels/vip", nil)
reqCookieRemove.AddCookie(&http.Cookie{Name: "cw_conversation", Value: authToken})
router.ServeHTTP(wCookieRemove, reqCookieRemove)
require.Equal(t, http.StatusNoContent, wCookieRemove.Code)
require.NoError(t, db.First(&conversation, conversation.ID).Error)
assert.Empty(t, conversation.Labels)
}
func TestWidgetHandler_ChatwootEvents_InvalidWebsiteTokenReturnsNotFound(t *testing.T) {
@@ -2130,6 +2403,25 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
assert.Equal(t, "Public Visitor", contactResp["name"])
assert.Equal(t, "visitor@public.test", contactResp["email"])
assert.NotEmpty(t, contactResp["pubsub_token"])
assertPublicContactFixtureShape(t, contactResp)
contactUpdateBody := map[string]any{
"name": "Updated Public Visitor",
"email": "Updated@Public.test",
"phone_number": "+15550002222",
}
contactUpdateJSON, _ := json.Marshal(contactUpdateBody)
w = httptest.NewRecorder()
req, _ = http.NewRequest("PATCH", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1", bytes.NewReader(contactUpdateJSON))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var contactUpdateResp map[string]any
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &contactUpdateResp))
assertPublicContactFixtureShape(t, contactUpdateResp)
assert.Equal(t, "Updated Public Visitor", contactUpdateResp["name"])
assert.Equal(t, "updated@public.test", contactUpdateResp["email"])
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"}}`)))
@@ -2141,6 +2433,7 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &conversationResp))
conversationID := strconv.FormatUint(uint64(conversationResp["id"].(float64)), 10)
assert.Equal(t, "open", conversationResp["status"])
assertPublicConversationFixtureShape(t, conversationResp)
messageBody := map[string]any{"content": "Hello from public API", "echo_id": "echo-1"}
messageJSON, _ := json.Marshal(messageBody)
@@ -2155,6 +2448,7 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
messageID := strconv.FormatUint(uint64(messageResp["id"].(float64)), 10)
assert.Equal(t, "Hello from public API", messageResp["content"])
assert.Equal(t, "incoming", messageResp["message_type"])
assertPublicMessageFixtureShape(t, messageResp)
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID+"/messages", nil)
@@ -2165,6 +2459,7 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &messagesResp))
require.Len(t, messagesResp, 1)
assert.Equal(t, "Hello from public API", messagesResp[0]["content"])
assertPublicMessageFixtureShape(t, messagesResp[0])
updateBody := map[string]any{"submitted_values": []map[string]any{{"name": "email", "value": "visitor@public.test"}}}
updateJSON, _ := json.Marshal(updateBody)
@@ -2175,6 +2470,7 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
assert.Equal(t, http.StatusOK, w.Code)
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &messageResp))
assertPublicMessageFixtureShape(t, messageResp)
attrs := messageResp["content_attributes"].(map[string]any)
assert.NotEmpty(t, attrs["submitted_values"])
@@ -2182,12 +2478,27 @@ func TestWidgetHandler_PublicAPIInboxContactConversationMessageFlow(t *testing.T
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)
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &conversationResp))
assertPublicConversationFixtureShape(t, conversationResp)
assert.Equal(t, "resolved", conversationResp["status"])
w = httptest.NewRecorder()
req, _ = http.NewRequest("GET", "/public/api/v1/inboxes/public-api-inbox/contacts/public-source-1/conversations/"+conversationID, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &conversationResp))
assertPublicConversationFixtureShape(t, conversationResp)
assert.Equal(t, "resolved", conversationResp["status"])
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)
var conversation model.Conversation
require.NoError(t, db.Where("inbox_id = ? AND contact_id = ?", inbox.ID, contactResp["id"]).First(&conversation).Error)
require.NotNil(t, conversation.ContactLastSeenAt)
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")
@@ -2521,3 +2832,68 @@ func TestWidgetHandler_PublicAPIMessageUpdate_CsatLocked(t *testing.T) {
require.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "You cannot update the CSAT survey after 14 days")
}
func assertWidgetConfigFixtureShape(t *testing.T, config map[string]interface{}) {
t.Helper()
for _, key := range []string{"auth_token", "website_token", "website_name", "welcome_tagline", "welcome_title", "widget_color"} {
assert.Contains(t, config, key, "widget config should expose %s", key)
}
}
func assertWidgetContactFixtureShape(t *testing.T, contact map[string]interface{}) {
t.Helper()
for _, key := range []string{"has_email", "has_name", "has_phone_number", "id", "identifier"} {
assert.Contains(t, contact, key, "widget contact should expose %s", key)
}
}
func assertWidgetConfigContactFixtureShape(t *testing.T, contact map[string]interface{}) {
t.Helper()
for _, key := range []string{"id", "identifier", "pubsub_token"} {
assert.Contains(t, contact, key, "widget config contact should expose %s", key)
}
}
func assertWidgetMessageFixtureShape(t *testing.T, message map[string]interface{}) {
t.Helper()
for _, key := range []string{"content", "content_attributes", "content_type", "conversation_id", "created_at", "id", "inbox_id", "message_type", "private", "source_id"} {
assert.Contains(t, message, key, "widget message should expose %s", key)
}
}
func assertWidgetAttachmentFixtureShape(t *testing.T, attachment map[string]interface{}) {
t.Helper()
for _, key := range []string{"account_id", "data_url", "extension", "file_size", "file_type", "height", "id", "message_id", "thumb_url", "width"} {
assert.Contains(t, attachment, key, "widget attachment should expose %s", key)
}
assert.NotContains(t, attachment, "created_at")
assert.NotContains(t, attachment, "updated_at")
}
func assertWidgetCampaignFixtureShape(t *testing.T, campaign map[string]interface{}) {
t.Helper()
for _, key := range []string{"id", "message", "sender", "trigger_only_during_business_hours", "trigger_rules"} {
assert.Contains(t, campaign, key, "widget campaign should expose %s", key)
}
}
func assertPublicContactFixtureShape(t *testing.T, contact map[string]any) {
t.Helper()
for _, key := range []string{"source_id", "pubsub_token", "id", "name", "email", "phone_number"} {
assert.Contains(t, contact, key, "public contact should expose %s", key)
}
}
func assertPublicConversationFixtureShape(t *testing.T, conversation map[string]any) {
t.Helper()
for _, key := range []string{"id", "uuid", "inbox_id", "contact_last_seen_at", "status", "agent_last_seen_at", "contact", "messages"} {
assert.Contains(t, conversation, key, "public conversation should expose %s", key)
}
}
func assertPublicMessageFixtureShape(t *testing.T, message map[string]any) {
t.Helper()
for _, key := range []string{"id", "content", "message_type", "content_type", "content_attributes", "created_at", "conversation_id"} {
assert.Contains(t, message, key, "public message should expose %s", key)
}
}