package v1 import ( "context" "encoding/json" "net/http" "net/http/httptest" "strconv" "strings" "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" ) // noopTypingIndicator is a stub that satisfies service.TypingIndicator type noopTypingIndicatorTheme struct{} func (n *noopTypingIndicatorTheme) SetTypingOn(_ context.Context, _ uint, _ uint, _ *ws.Performer) error { return nil } func (n *noopTypingIndicatorTheme) SetTypingOff(_ context.Context, _ uint, _ uint, _ *ws.Performer) error { return nil } type WebWidgetThemeHandlerTestSuite struct { suite.Suite router *gin.Engine themeHandler *WebWidgetThemeHandler preChatHandler *WebWidgetPreChatHandler db *gorm.DB widgetSvc *service.WidgetService inboxSvc *service.InboxService } func (s *WebWidgetThemeHandlerTestSuite) 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.Contact{}, &model.ContactInbox{}, &model.Conversation{}, &model.Message{}, &model.WidgetThemeConfig{}, &model.PreChatForm{}, ) 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) widgetSvc := service.NewWidgetService( inboxRepo, contactRepo, contactInboxRepo, conversationRepo, messageRepo, &noopTypingIndicatorTheme{}, themeConfigRepo, preChatFormRepo, fileUploadRepo, offlineMessageRepo, ) inboxSvc := service.NewInboxService(inboxRepo) s.widgetSvc = widgetSvc s.inboxSvc = inboxSvc s.themeHandler = NewWebWidgetThemeHandler(widgetSvc, inboxSvc) s.preChatHandler = NewWebWidgetPreChatHandler(widgetSvc, inboxSvc) // Setup router for theme handler s.router = gin.New() api := s.router.Group("/api/v1/accounts/:id") api.GET("/inboxes/:inbox_id/web_widget/theme_config", s.themeHandler.GetThemeConfig) api.PUT("/inboxes/:inbox_id/web_widget/theme_config", s.themeHandler.UpdateThemeConfig) api.DELETE("/inboxes/:inbox_id/web_widget/theme_config", s.themeHandler.DeleteThemeConfig) api.GET("/inboxes/:inbox_id/web_widget/pre_chat_form", s.preChatHandler.GetPreChatForm) api.PUT("/inboxes/:inbox_id/web_widget/pre_chat_form", s.preChatHandler.UpdatePreChatForm) api.DELETE("/inboxes/:inbox_id/web_widget/pre_chat_form", s.preChatHandler.DeletePreChatForm) } func (s *WebWidgetThemeHandlerTestSuite) TearDownSuite() { sqlDB, _ := s.db.DB() sqlDB.Close() } func (s *WebWidgetThemeHandlerTestSuite) seedAccountAndInbox() (uint, uint) { acc := &model.Account{Name: "ThemeTestOrg"} s.Require().NoError(s.db.Create(acc).Error) inbox := &model.Inbox{ AccountID: acc.ID, Name: "Widget Inbox", ChannelType: "web_widget", Enabled: true, } s.Require().NoError(s.db.Create(inbox).Error) return acc.ID, inbox.ID } func (s *WebWidgetThemeHandlerTestSuite) seedNonWidgetInbox(accountID uint) uint { inbox := &model.Inbox{ AccountID: accountID, Name: "Telegram Inbox", ChannelType: "telegram", Enabled: true, } s.Require().NoError(s.db.Create(inbox).Error) return inbox.ID } func accountURL(accountID, inboxID uint, suffix string) string { return "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/" + strconv.FormatUint(uint64(inboxID), 10) + "/web_widget/" + suffix } // --- Theme Config Tests --- func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_InvalidInboxID() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/abc/web_widget/theme_config" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_InvalidAccountID() { w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/inboxes/1/web_widget/theme_config", nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_InboxNotFound() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, 9999, "theme_config") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_NonWidgetChannel() { accountID, _ := s.seedAccountAndInbox() telegramInboxID := s.seedNonWidgetInbox(accountID) w := httptest.NewRecorder() url := accountURL(accountID, telegramInboxID, "theme_config") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_Success_NoConfig() { accountID, inboxID := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "theme_config") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetThemeConfig_Success_WithConfig() { accountID, inboxID := s.seedAccountAndInbox() // Seed a theme config directly in DB theme := &model.WidgetThemeConfig{ InboxID: inboxID, PrimaryColor: "#ff0000", BackgroundColor: "#ffffff", } s.Require().NoError(s.db.Create(theme).Error) w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "theme_config") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) var resp map[string]interface{} json.Unmarshal(w.Body.Bytes(), &resp) assert.NotNil(s.T(), resp["theme_config"]) } func (s *WebWidgetThemeHandlerTestSuite) TestUpdateThemeConfig_InvalidInboxID() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/abc/web_widget/theme_config" body := `{"primary_color": "#00ff00"}` req, _ := http.NewRequest("PUT", url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestUpdateThemeConfig_InboxNotFound() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, 9999, "theme_config") body := `{"primary_color": "#00ff00"}` req, _ := http.NewRequest("PUT", url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestUpdateThemeConfig_Success() { accountID, inboxID := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "theme_config") body := `{"primary_color": "#00ff00", "background_color": "#eeeeee"}` req, _ := http.NewRequest("PUT", url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestDeleteThemeConfig_InvalidInboxID() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/abc/web_widget/theme_config" req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestDeleteThemeConfig_Success() { accountID, inboxID := s.seedAccountAndInbox() // Seed theme config first theme := &model.WidgetThemeConfig{ InboxID: inboxID, PrimaryColor: "#ff0000", } s.Require().NoError(s.db.Create(theme).Error) w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "theme_config") req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } // --- Pre-Chat Form Tests --- func (s *WebWidgetThemeHandlerTestSuite) TestGetPreChatForm_InvalidInboxID() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/abc/web_widget/pre_chat_form" req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetPreChatForm_InboxNotFound() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, 9999, "pre_chat_form") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusNotFound, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetPreChatForm_NonWidgetChannel() { accountID, _ := s.seedAccountAndInbox() telegramInboxID := s.seedNonWidgetInbox(accountID) w := httptest.NewRecorder() url := accountURL(accountID, telegramInboxID, "pre_chat_form") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestGetPreChatForm_Success_NoConfig() { accountID, inboxID := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "pre_chat_form") req, _ := http.NewRequest("GET", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestUpdatePreChatForm_Success() { accountID, inboxID := s.seedAccountAndInbox() w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "pre_chat_form") body := `{"enabled": true, "message": "Please fill in", "require_name": true, "require_email": true}` req, _ := http.NewRequest("PUT", url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestUpdatePreChatForm_InvalidInboxID() { accountID, _ := s.seedAccountAndInbox() w := httptest.NewRecorder() url := "/api/v1/accounts/" + strconv.FormatUint(uint64(accountID), 10) + "/inboxes/abc/web_widget/pre_chat_form" body := `{"enabled": true}` req, _ := http.NewRequest("PUT", url, strings.NewReader(body)) req.Header.Set("Content-Type", "application/json") s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusBadRequest, w.Code) } func (s *WebWidgetThemeHandlerTestSuite) TestDeletePreChatForm_Success() { accountID, inboxID := s.seedAccountAndInbox() // Seed pre-chat form first form := &model.PreChatForm{ InboxID: inboxID, Enabled: true, Message: "Please fill in", RequireName: true, RequireEmail: true, } s.Require().NoError(s.db.Create(form).Error) w := httptest.NewRecorder() url := accountURL(accountID, inboxID, "pre_chat_form") req, _ := http.NewRequest("DELETE", url, nil) s.router.ServeHTTP(w, req) assert.Equal(s.T(), http.StatusOK, w.Code) } func TestWebWidgetThemeHandlerTestSuite(t *testing.T) { suite.Run(t, new(WebWidgetThemeHandlerTestSuite)) }