Files
gochat/backend/internal/handler/api/v1/facebook_callbacks_handler_test.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

207 lines
9.3 KiB
Go

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
}