second commit
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user