fix: restore web widget realtime replies (HH-556) (#136)

* fix: restore web widget realtime replies (HH-556)

* fix: make realtime delivery durable (HH-556)

* fix: make realtime message outbox atomic (HH-556)

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-23 22:35:10 +08:00
committed by GitHub
co-authored by rogee
parent 17244bcc9d
commit 60a6ac4785
13 changed files with 987 additions and 136 deletions
@@ -19,6 +19,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/datatypes"
@@ -28,6 +29,7 @@ import (
"github.com/gochat/gochat/internal/automation"
"github.com/gochat/gochat/internal/campaign"
"github.com/gochat/gochat/internal/channel"
"github.com/gochat/gochat/internal/config"
apiv1 "github.com/gochat/gochat/internal/handler/api/v1"
"github.com/gochat/gochat/internal/middleware"
@@ -35,7 +37,9 @@ import (
channelmodel "github.com/gochat/gochat/internal/model/channel"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
"github.com/gochat/gochat/internal/worker"
ws "github.com/gochat/gochat/internal/ws"
"github.com/gochat/gochat/internal/wsevent"
)
// noopTypingIndicatorWidget is a stub for handler tests.
@@ -2108,6 +2112,51 @@ func TestWidgetHandler_LegacySendMessageUsesWidgetPayload(t *testing.T) {
assert.Equal(t, float64(account.ID), attachment["account_id"])
}
func TestWidgetHandler_RealtimeFailureReturnsSuccessWithoutDuplicateMessage(t *testing.T) {
db, router, handler := setupWidgetHandlerTest(t)
require.NoError(t, db.AutoMigrate(&model.BackgroundJob{}))
seedWidgetHandlerData(t, db)
pool := worker.NewWorkerPoolWithOptions(db, worker.WithBackoff(func(int) time.Duration { return 0 }))
handler.widgetService.SetWorkerPool(pool)
rdb := redis.NewClient(&redis.Options{Addr: "127.0.0.1:1", MaxRetries: -1, DialTimeout: 10 * time.Millisecond})
t.Cleanup(func() { require.NoError(t, rdb.Close()) })
publisher := ws.NewEventPublisher(nil, nil, ws.NewBroadcastRelay(rdb, nil))
publisher.SetWorkerPool(pool)
dispatcher := channel.NewDispatcher()
dispatcher.Register(wsevent.New(publisher))
handler.widgetService.SetDispatcher(dispatcher)
initBody, err := json.Marshal(map[string]interface{}{"website_token": "handler_ws_token_123"})
require.NoError(t, err)
initResponse := httptest.NewRecorder()
initRequest := httptest.NewRequest(http.MethodPost, "/widget/init", bytes.NewReader(initBody))
initRequest.Header.Set("Content-Type", "application/json")
router.ServeHTTP(initResponse, initRequest)
require.Equal(t, http.StatusOK, initResponse.Code, initResponse.Body.String())
var initPayload map[string]interface{}
require.NoError(t, json.Unmarshal(initResponse.Body.Bytes(), &initPayload))
messageBody, err := json.Marshal(map[string]interface{}{"content": "persist once"})
require.NoError(t, err)
messageResponse := httptest.NewRecorder()
messageRequest := httptest.NewRequest(http.MethodPost, "/widget/messages", bytes.NewReader(messageBody))
messageRequest.Header.Set("Content-Type", "application/json")
messageRequest.Header.Set("X-Widget-Token", initPayload["widget_token"].(string))
router.ServeHTTP(messageResponse, messageRequest)
require.Equal(t, http.StatusOK, messageResponse.Code, messageResponse.Body.String())
processed, err := pool.ProcessOne(context.Background())
require.True(t, processed)
require.Error(t, err)
var messageCount int64
require.NoError(t, db.Model(&model.Message{}).Count(&messageCount).Error)
require.Equal(t, int64(1), messageCount)
var realtimeJob model.BackgroundJob
require.NoError(t, db.Where("queue = ? AND last_error != ''", "events").First(&realtimeJob).Error)
require.Equal(t, model.BackgroundJobStatusRetrying, realtimeJob.Status)
}
func TestWidgetHandler_SendMessage_NoWidgetToken(t *testing.T) {
_, router, _ := setupWidgetHandlerTest(t)