package v1 import ( "bytes" "context" "encoding/json" "errors" "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" facebookchannel "github.com/gochat/gochat/internal/channel/facebook" "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" ) type fakeFacebookCallbackProvider struct { longToken string pages []facebookchannel.FBPageInfo instagramID string tokenErr error pagesErr error instagramErr error } func (f *fakeFacebookCallbackProvider) ExchangeLongLivedUserToken(context.Context, string) (string, error) { if f.tokenErr != nil { return "", f.tokenErr } return f.longToken, nil } func (f *fakeFacebookCallbackProvider) ListFacebookPages(context.Context, string) ([]facebookchannel.FBPageInfo, error) { if f.pagesErr != nil { return nil, f.pagesErr } return f.pages, nil } func (f *fakeFacebookCallbackProvider) FetchInstagramBusinessAccountID(context.Context, string) (string, error) { if f.instagramErr != nil { return "", f.instagramErr } return f.instagramID, nil } func setupFacebookCallbackTest(t *testing.T, provider *fakeFacebookCallbackProvider) (*gin.Engine, *gorm.DB) { t.Helper() gin.SetMode(gin.TestMode) db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) require.NoError(t, err) require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &model.WorkingHour{}, &channelmodel.ChannelFacebook{})) inboxRepo := repository.NewInboxRepo(db) fbRepo := repository.NewChannelFacebookRepo(db) inboxSvc := service.NewInboxService(inboxRepo, nil, nil, nil, nil, nil, nil) fbSvc := service.NewChannelFacebookService(fbRepo) handler := NewFacebookChannelHandler(fbSvc, nil, inboxSvc, fbRepo, provider) r := gin.New() r.POST("/api/v1/accounts/:account_id/callbacks/register_facebook_page", handler.RegisterFacebookPage) r.GET("/api/v1/accounts/:account_id/callbacks/register_facebook_page", handler.RegisterFacebookPage) r.POST("/api/v1/accounts/:account_id/callbacks/facebook_pages", handler.FacebookPages) r.POST("/api/v1/accounts/:account_id/callbacks/facebook_pages.json", handler.FacebookPages) r.POST("/api/v1/accounts/:account_id/callbacks/reauthorize_page", handler.ReauthorizePage) return r, db } func TestFacebookCallbacks_RegisterFacebookPage(t *testing.T) { router, db := setupFacebookCallbackTest(t, &fakeFacebookCallbackProvider{instagramID: "ig-123"}) account := model.Account{Name: "Acme", Active: true} require.NoError(t, db.Create(&account).Error) body := map[string]any{"user_access_token": "user-token", "page_access_token": "page-token", "page_id": "page-1", "inbox_name": "Facebook"} rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/register_facebook_page", body) require.Equal(t, http.StatusOK, rec.Code) var payload map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload)) assert.Equal(t, "Facebook", payload["name"]) assert.Equal(t, "facebook", payload["channel_type"]) assert.Equal(t, "page-1", payload["page_id"]) assert.Equal(t, false, payload["enable_auto_assignment"]) assert.NotContains(t, payload, "success") assert.NotContains(t, payload, "data") var channel channelmodel.ChannelFacebook require.NoError(t, db.First(&channel).Error) assert.Equal(t, account.ID, channel.AccountID) assert.Equal(t, "user-token", channel.UserAccessToken) assert.Equal(t, "page-token", channel.PageAccessToken) assert.Equal(t, "ig-123", channel.InstagramBusinessAccountID) var inbox model.Inbox require.NoError(t, db.First(&inbox, uint(payload["id"].(float64))).Error) assert.Equal(t, account.ID, inbox.AccountID) assert.Equal(t, "Facebook", inbox.Name) assert.Equal(t, "facebook", inbox.ChannelType) assert.Equal(t, channel.ID, inbox.ChannelID) var config map[string]any require.NoError(t, json.Unmarshal([]byte(inbox.ChannelConfig), &config)) assert.Equal(t, "page-1", config["page_id"]) assert.Equal(t, "page-token", config["page_access_token"]) } func TestFacebookCallbacks_FacebookPagesMarksExistingPages(t *testing.T) { provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", pages: []facebookchannel.FBPageInfo{{ID: "page-1", Name: "Existing", AccessToken: "token-1"}, {ID: "page-2", Name: "New", AccessToken: "token-2"}}} router, db := setupFacebookCallbackTest(t, provider) account := model.Account{Name: "Acme", Active: true} require.NoError(t, db.Create(&account).Error) require.NoError(t, db.Create(&channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: 10, PageID: "page-1", PageAccessToken: "old"}).Error) rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/facebook_pages.json", map[string]any{"omniauth_token": "short-token"}) require.Equal(t, http.StatusOK, rec.Code) var payload struct { Data struct { UserAccessToken string `json:"user_access_token"` PageDetails []struct { ID string `json:"id"` Exists bool `json:"exists"` } `json:"page_details"` } `json:"data"` } require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload)) assert.Equal(t, "long-user-token", payload.Data.UserAccessToken) require.Len(t, payload.Data.PageDetails, 2) assert.True(t, payload.Data.PageDetails[0].Exists) assert.False(t, payload.Data.PageDetails[1].Exists) } func TestFacebookCallbacks_FacebookPagesReturns422ProviderFailure(t *testing.T) { provider := &fakeFacebookCallbackProvider{tokenErr: errors.New("facebook token exchange failed")} router, db := setupFacebookCallbackTest(t, provider) account := model.Account{Name: "Acme", Active: true} require.NoError(t, db.Create(&account).Error) rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/facebook_pages.json", map[string]any{"omniauth_token": "bad-token"}) require.Equal(t, http.StatusUnprocessableEntity, rec.Code) var payload map[string]any require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &payload)) assert.Equal(t, "facebook token exchange failed", payload["error"]) } func TestFacebookCallbacks_ReauthorizePageUpdatesMatchingPage(t *testing.T) { provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", instagramID: "ig-456", pages: []facebookchannel.FBPageInfo{{ID: "page-1", Name: "Renamed", AccessToken: "new-page-token"}}} router, db := setupFacebookCallbackTest(t, provider) account := model.Account{Name: "Acme", Active: true} require.NoError(t, db.Create(&account).Error) inbox := createTestChannelInbox(t, db, account.ID, 0, "Facebook", "facebook", map[string]any{"page_id": "page-1", "page_access_token": "old-page-token"}) channel := channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: inbox.ID, PageID: "page-1", PageAccessToken: "old-page-token", ReauthorizationRequired: true} require.NoError(t, db.Create(&channel).Error) inbox.ChannelID = channel.ID require.NoError(t, db.Save(inbox).Error) rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/reauthorize_page", map[string]any{"omniauth_token": "short-token", "inbox_id": inbox.ID}) require.Equal(t, http.StatusOK, rec.Code) var updated channelmodel.ChannelFacebook require.NoError(t, db.First(&updated, channel.ID).Error) assert.Equal(t, "long-user-token", updated.UserAccessToken) assert.Equal(t, "new-page-token", updated.PageAccessToken) assert.Equal(t, "Renamed", updated.PageName) assert.Equal(t, "ig-456", updated.InstagramBusinessAccountID) assert.False(t, updated.ReauthorizationRequired) var updatedInbox model.Inbox require.NoError(t, db.First(&updatedInbox, inbox.ID).Error) var config map[string]any require.NoError(t, json.Unmarshal([]byte(updatedInbox.ChannelConfig), &config)) assert.Equal(t, "new-page-token", config["page_access_token"]) assert.Equal(t, "Renamed", config["page_name"]) } func TestFacebookCallbacks_ReauthorizePageReturns422WhenPageMissing(t *testing.T) { provider := &fakeFacebookCallbackProvider{longToken: "long-user-token", pages: []facebookchannel.FBPageInfo{{ID: "other-page", AccessToken: "token"}}} router, db := setupFacebookCallbackTest(t, provider) account := model.Account{Name: "Acme", Active: true} require.NoError(t, db.Create(&account).Error) inbox := createTestChannelInbox(t, db, account.ID, 0, "Facebook", "facebook", map[string]any{"page_id": "page-1"}) require.NoError(t, db.Create(&channelmodel.ChannelFacebook{AccountID: account.ID, InboxID: inbox.ID, PageID: "page-1", PageAccessToken: "old"}).Error) rec := performFacebookCallbackRequest(t, router, http.MethodPost, "/api/v1/accounts/1/callbacks/reauthorize_page", map[string]any{"omniauth_token": "short-token", "inbox_id": inbox.ID}) assert.Equal(t, http.StatusUnprocessableEntity, rec.Code) } func performFacebookCallbackRequest(t *testing.T, router *gin.Engine, method, path string, body map[string]any) *httptest.ResponseRecorder { t.Helper() encoded, err := json.Marshal(body) require.NoError(t, err) req := httptest.NewRequest(method, path, bytes.NewReader(encoded)) req.Header.Set("Content-Type", "application/json") rec := httptest.NewRecorder() router.ServeHTTP(rec, req) return rec }