Files
Rogeeandrogee 3d9817c9f5 H-337: fix Web Channel availability and realtime delivery (#60)
* H-337: fix Web Channel availability and realtime delivery

* fix(widget): preserve realtime sender and activity contracts

* fix(widget): keep realtime sender payloads consistent

* fix(widget): make public message persistence atomic

* fix(inbox): keep availability projection out of schema

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-20 14:30:05 +08:00

1080 lines
45 KiB
Go

package widget
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
"github.com/gochat/gochat/internal/model"
)
// ---------- widgetConversationPayload (via GetConversations /widget/conversations) ----------
func TestWidgetHandler_Cov3_GetConversationsEmptyList_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/conversations", nil)
req.Header.Set("X-Auth-Token", authToken)
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))
convs, ok := resp["conversations"].([]interface{})
assert.True(t, ok, "should have conversations array")
assert.Empty(t, convs)
}
func TestWidgetHandler_Cov3_GetConversationsReturnsPayload_Cov3(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": "hello world"}})
wMsg := httptest.NewRecorder()
reqMsg, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMsg.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMsg, reqMsg)
require.Equal(t, http.StatusOK, wMsg.Code, wMsg.Body.String())
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/conversations", nil)
req.Header.Set("X-Auth-Token", authToken)
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))
convs, ok := resp["conversations"].([]interface{})
assert.True(t, ok, "should have conversations array")
require.Len(t, convs, 1)
convMap := convs[0].(map[string]interface{})
assert.NotNil(t, convMap["id"])
assert.NotNil(t, convMap["uuid"])
assert.Equal(t, float64(inbox.ID), convMap["inbox_id"])
assert.NotNil(t, convMap["status"])
assert.NotNil(t, convMap["custom_attributes"])
}
func TestWidgetHandler_Cov3_GetConversationsNoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/conversations", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_GetConversationsInvalidToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/conversations?widget_token=invalid", nil)
router.ServeHTTP(w, req)
// Invalid token returns 400 (generic error, not "invalid widget_token")
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest, "expected 401 or 400, got %d", w.Code)
}
// ---------- CreateConversation ----------
func TestWidgetHandler_Cov3_CreateConversation_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
body, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "hi"}})
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_CreateConversation_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Auth-Token", "some-token")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestWidgetHandler_Cov3_CreateConversation_InvalidToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
body, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "hi"}})
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations?cw_conversation=invalid-token", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestWidgetHandler_Cov3_CreateConversation_Success_Cov3(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)
w := httptest.NewRecorder()
body, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "new conversation message"}})
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations?cw_conversation="+authToken, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["id"])
msgs, ok := resp["messages"].([]interface{})
assert.True(t, ok, "should have messages array")
if len(msgs) > 0 {
msg := msgs[0].(map[string]interface{})
assert.Equal(t, "new conversation message", msg["content"])
}
var conv model.Conversation
require.NoError(t, db.Where("inbox_id = ?", inbox.ID).First(&conv).Error)
}
// ---------- PublicGetContact ----------
func TestWidgetHandler_Cov3_PublicGetContact_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, inbox, channelAPI := seedPublicAPIInbox(t, db)
createBody, _ := json.Marshal(map[string]interface{}{
"name": "Test Contact",
"email": "test@example.com",
"phone_number": "+1234567890",
})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code, wCreate.Body.String())
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID, nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, sourceID, resp["source_id"])
assert.Equal(t, "Test Contact", resp["name"])
assert.Equal(t, "test@example.com", resp["email"])
assert.Equal(t, "+1234567890", resp["phone_number"])
assert.NotNil(t, resp["id"])
assert.NotNil(t, resp["pubsub_token"])
_ = inbox
}
func TestWidgetHandler_Cov3_PublicGetContact_NotFound_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/nonexistent-source-id", nil)
router.ServeHTTP(w, req)
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusBadRequest, "expected 404 or 400, got %d: %s", w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicGetContact_InvalidInbox_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/nonexistent-inbox/contacts/some-id", nil)
router.ServeHTTP(w, req)
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusBadRequest, "expected 404 or 400, got %d: %s", w.Code, w.Body.String())
}
// ---------- GetCableToken ----------
func TestWidgetHandler_Cov3_GetCableToken_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/cable_token", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_GetCableToken_InvalidToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/cable_token?widget_token=invalid", nil)
router.ServeHTTP(w, req)
assert.True(t, w.Code == http.StatusUnauthorized || w.Code == http.StatusBadRequest, "expected 401 or 400, got %d", w.Code)
}
func TestWidgetHandler_Cov3_GetCableToken_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/cable_token?widget_token="+authToken, nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
// ---------- GetContact no token (chatwoot route → 404) ----------
func TestWidgetHandler_Cov3_GetContact_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/contact", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_GetContact_NoToken_WidgetRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/widget/contact", nil)
router.ServeHTTP(w, req)
// /widget/contact is not a registered route — 404 from router
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_GetContact_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/contact?cw_conversation="+authToken, nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["id"])
}
// ---------- UpdateContact no token (chatwoot route → 404) ----------
func TestWidgetHandler_Cov3_UpdateContact_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"name": "Test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/widget/contact", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_UpdateContact_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
body, _ := json.Marshal(map[string]interface{}{"name": "Updated Name"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/widget/contact?cw_conversation="+authToken, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["id"])
assert.Equal(t, true, resp["has_name"])
}
// ---------- UpdateContact invalid body ----------
func TestWidgetHandler_Cov3_UpdateContact_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/widget/contact?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- DestroyContactCustomAttributes ----------
func TestWidgetHandler_Cov3_DestroyContactAttrs_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"custom_attributes": []string{"key1"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/destroy_custom_attributes", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_DestroyContactAttrs_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/destroy_custom_attributes?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- SetUser ----------
func TestWidgetHandler_Cov3_SetUser_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"identifier": "test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/widget/contact/set_user", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_SetUser_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("PATCH", "/api/v1/widget/contact/set_user?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- ToggleTyping ----------
func TestWidgetHandler_Cov3_ToggleTyping_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/widget/conversations/1/toggle_typing", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_ToggleTyping_InvalidID_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/widget/conversations/abc/toggle_typing?cw_conversation=some-token", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestWidgetHandler_Cov3_ToggleTyping_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
// Send message to create conversation
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "hello"}})
wMsg := httptest.NewRecorder()
reqMsg, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMsg.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMsg, reqMsg)
require.Equal(t, http.StatusOK, wMsg.Code)
// Toggle typing without conversation ID (uses latest conversation)
w := httptest.NewRecorder()
body, _ := json.Marshal(map[string]interface{}{"typing_status": "on"})
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/toggle_typing?cw_conversation="+authToken, bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// ---------- UpdateLastSeen ----------
func TestWidgetHandler_Cov3_UpdateLastSeen_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/update_last_seen", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_UpdateLastSeen_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
// Send message to create conversation
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "hello"}})
wMsg := httptest.NewRecorder()
reqMsg, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMsg.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMsg, reqMsg)
require.Equal(t, http.StatusOK, wMsg.Code)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/update_last_seen?cw_conversation="+authToken, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// ---------- ToggleStatus ----------
func TestWidgetHandler_Cov3_ToggleStatus_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/conversations/toggle_status", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
func TestWidgetHandler_Cov3_ToggleStatus_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
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)
// Send message to create conversation
messageBody, _ := json.Marshal(map[string]interface{}{"message": map[string]interface{}{"content": "hello"}})
wMsg := httptest.NewRecorder()
reqMsg, _ := http.NewRequest("POST", "/api/v1/widget/messages?cw_conversation="+authToken, bytes.NewReader(messageBody))
reqMsg.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wMsg, reqMsg)
require.Equal(t, http.StatusOK, wMsg.Code)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/conversations/toggle_status?cw_conversation="+authToken, nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
// ---------- SetConversationCustomAttributes ----------
func TestWidgetHandler_Cov3_SetConvAttrs_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"custom_attributes": map[string]any{"key": "val"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/set_custom_attributes", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_SetConvAttrs_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/set_custom_attributes?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- DestroyConversationCustomAttributes ----------
func TestWidgetHandler_Cov3_DestroyConvAttrs_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"custom_attribute": []string{"key1"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/destroy_custom_attributes", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_DestroyConvAttrs_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/destroy_custom_attributes?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- ListInboxMembers ----------
func TestWidgetHandler_Cov3_ListInboxMembers_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/inbox_members", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_ListInboxMembers_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
seedWidgetHandlerData(t, db)
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, w.Body.String())
}
// ---------- ListCampaigns ----------
func TestWidgetHandler_Cov3_ListCampaigns_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/widget/campaigns", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_ListCampaigns_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
seedWidgetHandlerData(t, db)
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, w.Body.String())
}
// ---------- CreateEvent ----------
func TestWidgetHandler_Cov3_CreateEvent_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/events", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestWidgetHandler_Cov3_CreateEvent_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
seedWidgetHandlerData(t, db)
body, _ := json.Marshal(map[string]interface{}{"name": "page_view", "website_token": "handler_ws_token_123"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/events?website_token=handler_ws_token_123", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.True(t, w.Code == http.StatusNoContent || w.Code == http.StatusBadRequest, "expected 204 or 400, got %d: %s", w.Code, w.Body.String())
}
// ---------- AddLabel ----------
func TestWidgetHandler_Cov3_AddLabel_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"label": "test"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/labels", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
func TestWidgetHandler_Cov3_AddLabel_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/labels?cw_conversation=some-token", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- RemoveLabel ----------
func TestWidgetHandler_Cov3_RemoveLabel_NoToken_ChatwootRoute_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("DELETE", "/api/v1/widget/labels/1", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
// ---------- SendTranscript ----------
func TestWidgetHandler_Cov3_SendTranscript_NoToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/conversations/transcript", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusUnauthorized, w.Code)
}
// ---------- AddDyteParticipantToMeeting ----------
func TestWidgetHandler_Cov3_AddDyte_InvalidBody_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/widget/integrations/dyte/add_participant_to_meeting", bytes.NewReader([]byte("not json")))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// ---------- SubmitOfflineMessage ----------
func TestWidgetHandler_Cov3_SubmitOfflineMessage_NoWebsiteToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"message": "hello"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/widget/offline_message", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
func TestWidgetHandler_Cov3_SubmitOfflineMessage_InvalidWebsiteToken_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
body, _ := json.Marshal(map[string]interface{}{"message": "hello"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/widget/offline_message?website_token=invalid", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
}
// ---------- Public API endpoints ----------
func TestWidgetHandler_Cov3_PublicInboxShow_NotFound_Cov3(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/nonexistent", nil)
router.ServeHTTP(w, req)
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusBadRequest, "expected 404 or 400, got %d: %s", w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicInboxShow_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier, nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicCreateContact_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
body, _ := json.Marshal(map[string]interface{}{"name": "New Contact"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["source_id"])
assert.NotNil(t, resp["id"])
assert.Equal(t, "New Contact", resp["name"])
}
func TestWidgetHandler_PublicContactIgnoresCID(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
body, _ := json.Marshal(map[string]any{"name": "Visitor", "source_id": "visitor", "channel_metadata": map[string]any{"cid": "forged"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest(http.MethodPost, "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var contactInbox model.ContactInbox
require.NoError(t, db.Where("source_id = ?", "visitor").First(&contactInbox).Error)
assert.NotContains(t, string(contactInbox.ChannelMetadata), "forged")
}
func TestWidgetHandler_Cov3_PublicUpdateContact_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact first
createBody, _ := json.Marshal(map[string]interface{}{"name": "Original"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
// Update
updateBody, _ := json.Marshal(map[string]interface{}{"name": "Updated Name"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("PUT", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID, bytes.NewReader(updateBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, "Updated Name", resp["name"])
}
func TestWidgetHandler_Cov3_PublicListConversations_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
// List conversations (should be empty)
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicCreateConversation_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
// Create conversation
convBody, _ := json.Marshal(map[string]interface{}{"custom_attributes": map[string]any{"topic": "support"}})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["id"])
}
func TestWidgetHandler_Cov3_PublicToggleTyping_InvalidStatus_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
// Create conversation
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// Toggle typing with invalid status → returns 200 immediately
body, _ := json.Marshal(map[string]interface{}{"typing_status": "invalid"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID))+"/toggle_typing", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestWidgetHandler_Cov3_PublicToggleStatus_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact + conversation
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// Toggle status
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID))+"/toggle_status", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicUpdateLastSeen_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact + conversation
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// Update last seen
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID))+"/update_last_seen", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestWidgetHandler_Cov3_PublicListMessages_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact + conversation
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// List messages
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID))+"/messages", nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
}
func TestWidgetHandler_Cov3_PublicCreateMessage_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact + conversation
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// Create message
msgBody, _ := json.Marshal(map[string]interface{}{"content": "Hello from public API"})
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID))+"/messages", bytes.NewReader(msgBody))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.Equal(t, "Hello from public API", resp["content"])
}
func TestWidgetHandler_Cov3_PublicGetConversation_Success_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact + conversation
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
convBody, _ := json.Marshal(map[string]interface{}{})
wConv := httptest.NewRecorder()
reqConv, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations", bytes.NewReader(convBody))
reqConv.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wConv, reqConv)
require.Equal(t, http.StatusOK, wConv.Code)
var convResp map[string]interface{}
require.NoError(t, json.Unmarshal(wConv.Body.Bytes(), &convResp))
convID := convResp["id"].(float64)
// Get conversation
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/"+fmt.Sprint(uint(convID)), nil)
router.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code, w.Body.String())
var resp map[string]interface{}
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
assert.NotNil(t, resp["id"])
}
func TestWidgetHandler_Cov3_PublicUintParam_Invalid_Cov3(t *testing.T) {
db, router, _ := setupWidgetHandlerTest(t)
_, _, channelAPI := seedPublicAPIInbox(t, db)
// Create contact
createBody, _ := json.Marshal(map[string]interface{}{"name": "Test"})
wCreate := httptest.NewRecorder()
reqCreate, _ := http.NewRequest("POST", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts", bytes.NewReader(createBody))
reqCreate.Header.Set("Content-Type", "application/json")
router.ServeHTTP(wCreate, reqCreate)
require.Equal(t, http.StatusOK, wCreate.Code)
var createResp map[string]interface{}
require.NoError(t, json.Unmarshal(wCreate.Body.Bytes(), &createResp))
sourceID := createResp["source_id"].(string)
// Invalid conversation ID
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/public/api/v1/inboxes/"+channelAPI.Identifier+"/contacts/"+sourceID+"/conversations/abc", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
}
// Ensure imports are used
var _ = gorm.ErrRecordNotFound