351 lines
11 KiB
Plaintext
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))
|
|
} |