Files
gochat/internal/handler/api/v1/webwidget_offline_handler_test.go_BAK
T
2026-06-04 15:44:48 +08:00

351 lines
11 KiB
Plaintext

package v1
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/gochat/gochat/internal/model"
"github.com/gochat/gochat/internal/repository"
"github.com/gochat/gochat/internal/service"
ws "github.com/gochat/gochat/internal/ws"
)
// --- WebWidgetOfflineHandler Test Suite ---
// Uses real SQLite DB + real repo + real service (integration-style).
type WebWidgetOfflineHandlerTestSuite struct {
suite.Suite
router *gin.Engine
handler *WebWidgetOfflineHandler
db *gorm.DB
}
func (s *WebWidgetOfflineHandlerTestSuite) SetupSuite() {
gin.SetMode(gin.TestMode)
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
s.Require().NoError(err)
s.db = db
err = db.AutoMigrate(
&model.Account{},
&model.User{},
&model.Inbox{},
&model.WidgetOfflineMessage{},
)
s.Require().NoError(err)
// Create real repos
inboxRepo := repository.NewInboxRepo(db)
contactRepo := repository.NewContactRepo(db)
contactInboxRepo := repository.NewContactInboxRepo(db)
conversationRepo := repository.NewConversationRepo(db)
messageRepo := repository.NewMessageRepo(db)
themeConfigRepo := repository.NewWidgetThemeConfigRepo(db)
preChatFormRepo := repository.NewPreChatFormRepo(db)
fileUploadRepo := repository.NewWidgetFileUploadRepo(db)
offlineMessageRepo := repository.NewWidgetOfflineMessageRepo(db)
// Create real services — WidgetService needs a TypingIndicator.
// For handler tests we only exercise offline-message paths which never call typing,
// so we pass a nil-safe stub. However WidgetService stores it as interface;
// nil would panic if accidentally called. Use a no-op stub.
widgetSvc := service.NewWidgetService(
inboxRepo,
contactRepo,
contactInboxRepo,
conversationRepo,
messageRepo,
&noopTypingIndicator{},
themeConfigRepo,
preChatFormRepo,
fileUploadRepo,
offlineMessageRepo,
)
inboxSvc := service.NewInboxService(inboxRepo)
// Create handler
s.handler = NewWebWidgetOfflineHandler(widgetSvc, inboxSvc)
// Setup router
s.router = gin.New()
accountsGroup := s.router.Group("/api/v1/accounts/:id")
{
accountsGroup.GET("/web_widgets/offline_messages", s.handler.ListOfflineMessages)
accountsGroup.GET("/inboxes/:inbox_id/web_widget/offline_messages", s.handler.ListOfflineMessagesByInbox)
accountsGroup.PUT("/web_widgets/offline_messages/:offline_message_id/dismiss", s.handler.DismissOfflineMessage)
}
}
func (s *WebWidgetOfflineHandlerTestSuite) TearDownSuite() {
sqlDB, err := s.db.DB()
s.Require().NoError(err)
sqlDB.Close()
}
// Helper: create prerequisite Account + Inbox
func (s *WebWidgetOfflineHandlerTestSuite) createTestAccountAndInbox() (*model.Account, *model.Inbox) {
account := &model.Account{Name: "OfflineTestOrg", Locale: "en"}
s.Require().NoError(s.db.Create(account).Error)
inbox := &model.Inbox{
AccountID: account.ID,
Name: "Widget Inbox",
ChannelType: "web_widget",
ChannelID: 0,
Enabled: true,
}
s.Require().NoError(s.db.Create(inbox).Error)
return account, inbox
}
// Helper: seed offline messages
func (s *WebWidgetOfflineHandlerTestSuite) createOfflineMessage(accountID, inboxID uint, content string) *model.WidgetOfflineMessage {
msg := &model.WidgetOfflineMessage{
InboxID: inboxID,
AccountID: accountID,
ContactName: "Visitor",
Content: content,
Status: model.OfflineStatusPending,
}
s.Require().NoError(s.db.Create(msg).Error)
return msg
}
// ========== ListOfflineMessages ==========
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessages_Success() {
account, inbox := s.createTestAccountAndInbox()
_ = s.createOfflineMessage(account.ID, inbox.ID, "Hello, anyone there?")
_ = s.createOfflineMessage(account.ID, inbox.ID, "I need help")
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) + "/web_widgets/offline_messages?page=1&page_size=25"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
msgs := resp["offline_messages"].([]interface{})
assert.Equal(s.T(), 2, len(msgs))
meta := resp["meta"].(map[string]interface{})
assert.Equal(s.T(), float64(1), meta["page"])
assert.Equal(s.T(), float64(25), meta["page_size"])
assert.Equal(s.T(), float64(2), meta["total_count"])
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessages_InvalidAccountID() {
w := httptest.NewRecorder()
url := "/api/v1/accounts/invalid/web_widgets/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "invalid account_id", resp["error"])
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessages_EmptyResult() {
account, _ := s.createTestAccountAndInbox()
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) + "/web_widgets/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
msgs := resp["offline_messages"].([]interface{})
assert.Equal(s.T(), 0, len(msgs))
}
// ========== ListOfflineMessagesByInbox ==========
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessagesByInbox_Success() {
account, inbox := s.createTestAccountAndInbox()
_ = s.createOfflineMessage(account.ID, inbox.ID, "Offline msg A")
_ = s.createOfflineMessage(account.ID, inbox.ID, "Offline msg B")
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/inboxes/" + strconv.FormatUint(uint64(inbox.ID), 10) + "/web_widget/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
msgs := resp["offline_messages"].([]interface{})
assert.Equal(s.T(), 2, len(msgs))
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessagesByInbox_InvalidInboxID() {
account, _ := s.createTestAccountAndInbox()
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/inboxes/invalid/web_widget/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "invalid inbox_id", resp["error"])
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessagesByInbox_InvalidAccountID() {
w := httptest.NewRecorder()
url := "/api/v1/accounts/invalid/inboxes/1/web_widget/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "invalid account_id", resp["error"])
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessagesByInbox_InboxNotFound() {
account, _ := s.createTestAccountAndInbox()
w := httptest.NewRecorder()
// Use a non-existent inbox ID under a valid account
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/inboxes/99999/web_widget/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusNotFound, w.Code)
}
func (s *WebWidgetOfflineHandlerTestSuite) TestListOfflineMessagesByInbox_NonWebWidgetInbox() {
account := &model.Account{Name: "NonWidgetOrg", Locale: "en"}
s.Require().NoError(s.db.Create(account).Error)
// Create a non-web_widget inbox (e.g. facebook)
inbox := &model.Inbox{
AccountID: account.ID,
Name: "FB Inbox",
ChannelType: "facebook",
ChannelID: 0,
Enabled: true,
}
s.Require().NoError(s.db.Create(inbox).Error)
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/inboxes/" + strconv.FormatUint(uint64(inbox.ID), 10) + "/web_widget/offline_messages"
req, _ := http.NewRequest("GET", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "offline messages only supported for web_widget channel", resp["error"])
}
// ========== DismissOfflineMessage ==========
func (s *WebWidgetOfflineHandlerTestSuite) TestDismissOfflineMessage_Success() {
account, inbox := s.createTestAccountAndInbox()
msg := s.createOfflineMessage(account.ID, inbox.ID, "Please dismiss me")
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/web_widgets/offline_messages/" + strconv.FormatUint(uint64(msg.ID), 10) + "/dismiss"
req, _ := http.NewRequest("PUT", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusOK, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "offline message dismissed", resp["message"])
// Verify DB status changed to dismissed
var updated model.WidgetOfflineMessage
s.Require().NoError(s.db.First(&updated, msg.ID).Error)
assert.Equal(s.T(), model.OfflineStatusDismissed, updated.Status)
}
func (s *WebWidgetOfflineHandlerTestSuite) TestDismissOfflineMessage_InvalidMessageID() {
account, _ := s.createTestAccountAndInbox()
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/web_widgets/offline_messages/invalid/dismiss"
req, _ := http.NewRequest("PUT", url, nil)
s.router.ServeHTTP(w, req)
assert.Equal(s.T(), http.StatusBadRequest, w.Code)
var resp map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &resp)
assert.NoError(s.T(), err)
assert.Equal(s.T(), "invalid offline_message_id", resp["error"])
}
func (s *WebWidgetOfflineHandlerTestSuite) TestDismissOfflineMessage_NonExistentMessage() {
account, _ := s.createTestAccountAndInbox()
w := httptest.NewRecorder()
url := "/api/v1/accounts/" + strconv.FormatUint(uint64(account.ID), 10) +
"/web_widgets/offline_messages/99999/dismiss"
req, _ := http.NewRequest("PUT", url, nil)
s.router.ServeHTTP(w, req)
// Service will return error for non-existent record → 500
assert.Equal(s.T(), http.StatusInternalServerError, w.Code)
}
// noopTypingIndicator is a stub that satisfies service.TypingIndicator
// without doing anything. Used in tests where typing paths are never exercised.
type noopTypingIndicator struct{}
func (n *noopTypingIndicator) SetTypingOn(_ context.Context, _ uint, _ uint, _ *ws.Performer) error {
return nil
}
func (n *noopTypingIndicator) SetTypingOff(_ context.Context, _ uint, _ uint, _ *ws.Performer) error {
return nil
}
func TestWebWidgetOfflineHandlerTestSuite(t *testing.T) {
suite.Run(t, new(WebWidgetOfflineHandlerTestSuite))
}