package v1 import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" whatsappchannel "github.com/gochat/gochat/internal/channel/whatsapp" "github.com/gochat/gochat/internal/model" 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" ) // setupInboxMemberActionRouter creates a test router with inbox member-action routes. // We use nil InboxService since we only test param validation (bad IDs). // Body/service-level logic is covered in service tests with real DB. func setupInboxMemberActionRouter(handler *InboxHandler) *gin.Engine { gin.SetMode(gin.TestMode) r := gin.New() r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/set_agent_bot", handler.SetAgentBot) r.GET("/api/v1/accounts/:account_id/inboxes/:inbox_id/health", handler.Health) r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/sync_templates", handler.SyncTemplates) r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/register_webhook", handler.RegisterWebhook) r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/enable_whatsapp_calling", handler.EnableWhatsAppCalling) r.POST("/api/v1/accounts/:account_id/inboxes/:inbox_id/disable_whatsapp_calling", handler.DisableWhatsAppCalling) return r } func newNilInboxHandler() *InboxHandler { return NewInboxHandler(&service.InboxService{}) } func setupInboxListAccessRouter(t *testing.T) (*gin.Engine, *gorm.DB, *model.Account, *model.User) { t.Helper() db, err := gorm.Open(sqlite.Open("file:inbox_list_access?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.User{}, &model.Inbox{}, &model.InboxMember{}, &model.WorkingHour{})) account := &model.Account{Name: "Inbox List Access", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) user := &model.User{Name: "Scoped Agent", Email: "handler-scoped-agent@example.com"} require.NoError(t, db.Create(user).Error) handler := NewInboxHandler(service.NewInboxService(repository.NewInboxRepo(db), nil, nil, nil, nil, nil, nil)) router := gin.New() router.Use(func(c *gin.Context) { c.Set("user_id", user.ID) c.Set("role", "agent") c.Next() }) router.GET("/api/v1/accounts/:account_id/inboxes", handler.List) return router, db, account, user } func TestInboxHandler_ListScopesAgentToAssignedInboxes(t *testing.T) { router, db, account, user := setupInboxListAccessRouter(t) assigned := &model.Inbox{AccountID: account.ID, Name: "Assigned", ChannelType: "web_widget", ChannelID: 1} hidden := &model.Inbox{AccountID: account.ID, Name: "Hidden", ChannelType: "api", ChannelID: 2} require.NoError(t, db.Create(assigned).Error) require.NoError(t, db.Create(hidden).Error) require.NoError(t, db.Create(&model.InboxMember{InboxID: assigned.ID, UserID: user.ID}).Error) w := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/v1/accounts/1/inboxes", nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var body struct { Payload []map[string]any `json:"payload"` } require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) require.Len(t, body.Payload, 1) assert.Equal(t, "Assigned", body.Payload[0]["name"]) } type fakeInboxHandlerWhatsAppService struct{} func (f *fakeInboxHandlerWhatsAppService) FetchMessageTemplates(context.Context, *channelmodel.ChannelWhatsApp) ([]interface{}, error) { return nil, nil } func (f *fakeInboxHandlerWhatsAppService) FetchHealthStatus(context.Context, *channelmodel.ChannelWhatsApp) (map[string]interface{}, error) { return map[string]interface{}{}, nil } func (f *fakeInboxHandlerWhatsAppService) SetupWebhook(context.Context, *channelmodel.ChannelWhatsApp, string) error { return nil } func (f *fakeInboxHandlerWhatsAppService) SetupWebhookFields(context.Context, *channelmodel.ChannelWhatsApp, string, []string) error { return nil } func (f *fakeInboxHandlerWhatsAppService) UpdateCallingStatus(context.Context, *channelmodel.ChannelWhatsApp, string) error { return nil } // parseJSONResponse extracts the "error" key from a JSON response body. func parseJSONError(body []byte) string { var resp map[string]interface{} if err := json.Unmarshal(body, &resp); err != nil { panic(err) } if v, ok := resp["error"]; ok { return v.(string) } return "" } // ======================================== // SetAgentBot — param validation tests // ======================================== func TestInboxSetAgentBot_BadAccountID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/inboxes/5/set_agent_bot", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid account id") } func TestInboxSetAgentBot_BadInboxID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/xyz/set_agent_bot", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id") } // ======================================== // Health — param validation tests // ======================================== func TestInboxHealth_BadAccountID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/abc/inboxes/5/health", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid account id") } func TestInboxHealth_BadInboxID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("GET", "/api/v1/accounts/1/inboxes/xyz/health", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id") } // ======================================== // SyncTemplates — param validation tests // ======================================== func TestInboxSyncTemplates_BadAccountID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/inboxes/5/sync_templates", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid account id") } func TestInboxSyncTemplates_BadInboxID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/xyz/sync_templates", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id") } func TestInboxSyncTemplates_ChatwootQueuedResponse(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:inbox_sync_templates_handler?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) t.Cleanup(func() { sqlDB, dbErr := db.DB() if dbErr == nil { _ = sqlDB.Close() } }) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &channelmodel.ChannelWhatsApp{}, &model.BackgroundJob{})) account := &model.Account{Name: "Sync Templates", Locale: "en", Active: true} require.NoError(t, db.Create(account).Error) inbox := &model.Inbox{AccountID: account.ID, Name: "WhatsApp", ChannelType: "whatsapp", ChannelID: 1} require.NoError(t, db.Create(inbox).Error) channel := &channelmodel.ChannelWhatsApp{AccountID: account.ID, InboxID: inbox.ID, PhoneNumber: "+1555010000", PhoneNumberID: "phone-1", BusinessAccountID: "waba-1", AccessToken: "token", Provider: "whatsapp_cloud"} require.NoError(t, db.Create(channel).Error) wp := worker.NewWorkerPool(db) svc := service.NewInboxService(repository.NewInboxRepo(db), nil, nil, nil, nil, nil, whatsappchannel.NewRepository(db)) svc.SetWorkerPool(wp) handler := NewInboxHandler(svc) router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/1/sync_templates", bytes.NewReader(nil)) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) var body map[string]any require.NoError(t, json.Unmarshal(w.Body.Bytes(), &body)) assert.Equal(t, service.InboxTemplateSyncInitiatedMessage, body["message"]) assert.NotContains(t, body, "templates") } // ======================================== // RegisterWebhook — param validation tests // ======================================== func TestInboxRegisterWebhook_BadAccountID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/inboxes/5/register_webhook", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid account id") } func TestInboxRegisterWebhook_BadInboxID(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/xyz/register_webhook", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id") } // ======================================== // WhatsApp calling — param and response parity tests // ======================================== func TestInboxWhatsAppCalling_BadParams(t *testing.T) { handler := newNilInboxHandler() router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/abc/inboxes/5/enable_whatsapp_calling", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid account id") w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/api/v1/accounts/1/inboxes/xyz/disable_whatsapp_calling", nil) router.ServeHTTP(w, req) assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, parseJSONError(w.Body.Bytes()), "invalid inbox id") } func TestInboxWhatsAppCalling_EnableDisableReturnEmptyOK(t *testing.T) { db, err := gorm.Open(sqlite.Open("file:inbox_whatsapp_calling_handler?mode=memory&cache=shared"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) t.Cleanup(func() { sqlDB, dbErr := db.DB() if dbErr == nil { _ = sqlDB.Close() } }) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &channelmodel.ChannelWhatsApp{})) account := &model.Account{Name: "Calling", Locale: "en", Active: true, FeatureFlags: `{"channel_voice":true}`} require.NoError(t, db.Create(account).Error) inbox := &model.Inbox{AccountID: account.ID, Name: "WhatsApp", ChannelType: "whatsapp", ChannelID: 1} require.NoError(t, db.Create(inbox).Error) channel := &channelmodel.ChannelWhatsApp{AccountID: account.ID, InboxID: inbox.ID, PhoneNumber: "+1555010000", PhoneNumberID: "phone-1", BusinessAccountID: "waba-1", AccessToken: "token", Provider: "whatsapp_cloud"} require.NoError(t, db.Create(channel).Error) svc := service.NewInboxService(repository.NewInboxRepo(db), nil, nil, nil, nil, &fakeInboxHandlerWhatsAppService{}, whatsappchannel.NewRepository(db)) handler := NewInboxHandler(svc) router := setupInboxMemberActionRouter(handler) w := httptest.NewRecorder() req, _ := http.NewRequest("POST", "/api/v1/accounts/1/inboxes/1/enable_whatsapp_calling", nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) assert.Empty(t, w.Body.String()) w = httptest.NewRecorder() req, _ = http.NewRequest("POST", "/api/v1/accounts/1/inboxes/1/disable_whatsapp_calling", nil) router.ServeHTTP(w, req) require.Equal(t, http.StatusOK, w.Code, w.Body.String()) assert.Empty(t, w.Body.String()) }