212 lines
9.2 KiB
Go
212 lines
9.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
|
|
whatsapp "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"
|
|
)
|
|
|
|
type fakeWhatsAppAuthorizationChannelService struct {
|
|
webhookURL string
|
|
}
|
|
|
|
func (f *fakeWhatsAppAuthorizationChannelService) FetchMessageTemplates(context.Context, *channelmodel.ChannelWhatsApp) ([]interface{}, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (f *fakeWhatsAppAuthorizationChannelService) FetchHealthStatus(context.Context, *channelmodel.ChannelWhatsApp) (map[string]interface{}, error) {
|
|
return map[string]interface{}{}, nil
|
|
}
|
|
|
|
func (f *fakeWhatsAppAuthorizationChannelService) SetupWebhook(_ context.Context, _ *channelmodel.ChannelWhatsApp, webhookURL string) error {
|
|
f.webhookURL = webhookURL
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWhatsAppAuthorizationChannelService) SetupWebhookFields(_ context.Context, _ *channelmodel.ChannelWhatsApp, webhookURL string, _ []string) error {
|
|
f.webhookURL = webhookURL
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeWhatsAppAuthorizationChannelService) UpdateCallingStatus(context.Context, *channelmodel.ChannelWhatsApp, string) error {
|
|
return nil
|
|
}
|
|
|
|
func setupWhatsAppAuthorizationService(t *testing.T) (*InboxService, *gorm.DB, *fakeWhatsAppAuthorizationChannelService) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.AutoMigrate(&model.Account{}, &model.Inbox{}, &channelmodel.ChannelWhatsApp{}))
|
|
fake := &fakeWhatsAppAuthorizationChannelService{}
|
|
svc := NewInboxService(repository.NewInboxRepo(db), nil, nil, nil, nil, fake, whatsapp.NewRepository(db))
|
|
return svc, db, fake
|
|
}
|
|
|
|
func withWhatsAppAuthorizationHTTPClient(t *testing.T, fn func(*http.Request) (int, map[string]any)) {
|
|
t.Helper()
|
|
original := whatsappAuthorizationHTTPClient
|
|
whatsappAuthorizationHTTPClient = &http.Client{Transport: whatsappAuthorizationRoundTripFunc(func(req *http.Request) (*http.Response, error) {
|
|
status, payload := fn(req)
|
|
body, _ := json.Marshal(payload)
|
|
return &http.Response{StatusCode: status, Body: io.NopCloser(strings.NewReader(string(body))), Header: http.Header{}}, nil
|
|
})}
|
|
t.Cleanup(func() { whatsappAuthorizationHTTPClient = original })
|
|
}
|
|
|
|
func TestWhatsAppAuthorization_CreateEmbeddedSignupInbox(t *testing.T) {
|
|
t.Setenv("WHATSAPP_GRAPH_API_BASE", "https://graph.example.test")
|
|
t.Setenv("WHATSAPP_API_VERSION", "v22.0")
|
|
t.Setenv("WHATSAPP_APP_ID", "app-id")
|
|
t.Setenv("WHATSAPP_APP_SECRET", "app-secret")
|
|
t.Setenv("FRONTEND_URL", "https://app.example.test")
|
|
svc, db, fake := setupWhatsAppAuthorizationService(t)
|
|
account := &model.Account{Name: "Acme", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
|
|
withWhatsAppAuthorizationHTTPClient(t, func(req *http.Request) (int, map[string]any) {
|
|
switch req.URL.Path {
|
|
case "/v22.0/oauth/access_token":
|
|
assert.Equal(t, "app-id", req.URL.Query().Get("client_id"))
|
|
assert.Equal(t, "auth-code", req.URL.Query().Get("code"))
|
|
return http.StatusOK, map[string]any{"access_token": "access-token"}
|
|
case "/v22.0/waba-1/phone_numbers":
|
|
assert.Equal(t, "access-token", req.URL.Query().Get("access_token"))
|
|
return http.StatusOK, map[string]any{"data": []any{map[string]any{"id": "phone-1", "display_phone_number": "+1 (555) 010-000", "verified_name": "Acme Support", "code_verification_status": "VERIFIED"}}}
|
|
case "/v22.0/debug_token":
|
|
return http.StatusOK, map[string]any{
|
|
"data": map[string]any{
|
|
"granular_scopes": []any{
|
|
map[string]any{
|
|
"scope": "whatsapp_business_management",
|
|
"target_ids": []any{"waba-1"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected request path %s", req.URL.Path)
|
|
return http.StatusNotFound, map[string]any{}
|
|
}
|
|
})
|
|
|
|
result, err := svc.AuthorizeWhatsAppEmbeddedSignup(context.Background(), account.ID, WhatsAppAuthorizationRequest{Code: "auth-code", BusinessID: "business-1", WabaID: "waba-1", PhoneNumberID: "phone-1"})
|
|
require.NoError(t, err)
|
|
require.NotNil(t, result.Inbox)
|
|
assert.Equal(t, "Acme Support WhatsApp", result.Inbox.Name)
|
|
assert.Equal(t, "whatsapp", result.Inbox.ChannelType)
|
|
assert.Equal(t, "https://app.example.test/webhooks/whatsapp/+1555010000", fake.webhookURL)
|
|
|
|
var channel channelmodel.ChannelWhatsApp
|
|
require.NoError(t, db.First(&channel, result.Inbox.ChannelID).Error)
|
|
assert.Equal(t, account.ID, channel.AccountID)
|
|
assert.Equal(t, result.Inbox.ID, channel.InboxID)
|
|
assert.Equal(t, "+1555010000", channel.PhoneNumber)
|
|
assert.Equal(t, "phone-1", channel.PhoneNumberID)
|
|
assert.Equal(t, "waba-1", channel.BusinessAccountID)
|
|
assert.Equal(t, "whatsapp_cloud", channel.Provider)
|
|
providerConfig := parseJSONMap(channel.ProviderConfig)
|
|
assert.Equal(t, "access-token", providerConfig["api_key"])
|
|
assert.Equal(t, "embedded_signup", providerConfig["source"])
|
|
}
|
|
|
|
func TestWhatsAppAuthorization_ReauthorizesExistingInbox(t *testing.T) {
|
|
t.Setenv("WHATSAPP_GRAPH_API_BASE", "https://graph.example.test")
|
|
t.Setenv("FRONTEND_URL", "https://app.example.test")
|
|
svc, db, _ := setupWhatsAppAuthorizationService(t)
|
|
account := &model.Account{Name: "Acme", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
inbox := &model.Inbox{AccountID: account.ID, Name: "Old WhatsApp", ChannelType: "whatsapp", Enabled: true}
|
|
require.NoError(t, db.Create(inbox).Error)
|
|
channel := &channelmodel.ChannelWhatsApp{AccountID: account.ID, InboxID: inbox.ID, PhoneNumber: "+1555010000", PhoneNumberID: "old-phone", BusinessAccountID: "old-waba", AccessToken: "old-token", Provider: "whatsapp_cloud", ReauthorizationRequired: true}
|
|
require.NoError(t, whatsapp.NewRepository(db).Create(context.Background(), channel))
|
|
inbox.ChannelID = channel.ID
|
|
require.NoError(t, db.Save(inbox).Error)
|
|
|
|
withWhatsAppAuthorizationHTTPClient(t, func(req *http.Request) (int, map[string]any) {
|
|
switch req.URL.Path {
|
|
case "/v22.0/oauth/access_token":
|
|
return http.StatusOK, map[string]any{"access_token": "new-token"}
|
|
case "/v22.0/waba-new/phone_numbers":
|
|
return http.StatusOK, map[string]any{"data": []any{map[string]any{"id": "phone-new", "display_phone_number": "+1 (555) 010-000", "verified_name": "New Name"}}}
|
|
case "/v22.0/debug_token":
|
|
return http.StatusOK, map[string]any{
|
|
"data": map[string]any{
|
|
"granular_scopes": []any{
|
|
map[string]any{
|
|
"scope": "whatsapp_business_management",
|
|
"target_ids": []any{"waba-new"},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
default:
|
|
t.Fatalf("unexpected request path %s", req.URL.Path)
|
|
return http.StatusNotFound, map[string]any{}
|
|
}
|
|
})
|
|
|
|
result, err := svc.AuthorizeWhatsAppEmbeddedSignup(context.Background(), account.ID, WhatsAppAuthorizationRequest{Code: "code", BusinessID: "business-new", WabaID: "waba-new", PhoneNumberID: "phone-new", InboxID: &inbox.ID})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "Inbox reauthorized successfully", result.Message)
|
|
|
|
var updated channelmodel.ChannelWhatsApp
|
|
require.NoError(t, db.First(&updated, channel.ID).Error)
|
|
assert.Equal(t, "new-token", updated.AccessToken)
|
|
assert.Equal(t, "phone-new", updated.PhoneNumberID)
|
|
assert.Equal(t, "business-new", updated.BusinessAccountID)
|
|
assert.False(t, updated.ReauthorizationRequired)
|
|
}
|
|
|
|
func TestWhatsAppAuthorization_ValidationAndProviderErrors(t *testing.T) {
|
|
svc, db, _ := setupWhatsAppAuthorizationService(t)
|
|
account := &model.Account{Name: "Acme", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
|
|
_, err := svc.AuthorizeWhatsAppEmbeddedSignup(context.Background(), account.ID, WhatsAppAuthorizationRequest{BusinessID: "business", WabaID: "waba"})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "code")
|
|
|
|
t.Setenv("WHATSAPP_GRAPH_API_BASE", "https://graph.example.test")
|
|
withWhatsAppAuthorizationHTTPClient(t, func(req *http.Request) (int, map[string]any) {
|
|
return http.StatusUnprocessableEntity, map[string]any{"error": "bad code"}
|
|
})
|
|
_, err = svc.AuthorizeWhatsAppEmbeddedSignup(context.Background(), account.ID, WhatsAppAuthorizationRequest{Code: "bad", BusinessID: "business", WabaID: "waba"})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "Token exchange failed")
|
|
}
|
|
|
|
func TestWhatsAppAuthorization_MissingReauthorizationInboxStopsBeforeProviderCalls(t *testing.T) {
|
|
svc, db, _ := setupWhatsAppAuthorizationService(t)
|
|
account := &model.Account{Name: "Acme", Active: true}
|
|
require.NoError(t, db.Create(account).Error)
|
|
missingInboxID := uint(999)
|
|
|
|
withWhatsAppAuthorizationHTTPClient(t, func(req *http.Request) (int, map[string]any) {
|
|
t.Fatalf("unexpected provider request path %s", req.URL.Path)
|
|
return http.StatusInternalServerError, map[string]any{}
|
|
})
|
|
|
|
_, err := svc.AuthorizeWhatsAppEmbeddedSignup(context.Background(), account.ID, WhatsAppAuthorizationRequest{Code: "code", BusinessID: "business", WabaID: "waba", InboxID: &missingInboxID})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "record not found")
|
|
}
|
|
|
|
type whatsappAuthorizationRoundTripFunc func(*http.Request) (*http.Response, error)
|
|
|
|
func (f whatsappAuthorizationRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
return f(req)
|
|
}
|