* H-300: wire Captain Skills into Web runtime * H-300: enforce effective model and conservative skill budget * H-300: fix CI gosec step * ci: extend golangci-lint timeout * fix lint findings across backend * fix(push): resolve delivery protocol blockers * test(repository): close SQLite test databases * test(repository): reuse SQLite schema per package * H-307: restore backend Go cache in CI * H-307: prefetch modules before cold lint * H-307: resolve govulncheck security gate * H-307: build lint with patched Go toolchain * H-307: clear remaining security scan findings --------- Co-authored-by: Rogee <rogee@ipao.vip>
3492 lines
110 KiB
Go
3492 lines
110 KiB
Go
package facebook
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gochat/gochat/internal/channel"
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// ===========================
|
|
// InstagramProvider — RegisterWebhook (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_RegisterWebhook_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodPost, r.Method)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"success": true})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "test_app"
|
|
|
|
result, err := p.RegisterWebhook(context.Background(), "token", "page123")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result, "page123")
|
|
}
|
|
|
|
func TestInstagramProvider_RegisterWebhook_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RegisterWebhook(context.Background(), "token", "page123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 500")
|
|
}
|
|
|
|
func TestInstagramProvider_RegisterWebhook_BadJSON_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
if _, err := w.Write([]byte("not json")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RegisterWebhook(context.Background(), "token", "page123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "parse")
|
|
}
|
|
|
|
func TestInstagramProvider_RegisterWebhook_SuccessFalse_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"success": false})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RegisterWebhook(context.Background(), "token", "page123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "success=false")
|
|
}
|
|
|
|
func TestInstagramProvider_RegisterWebhook_Status201_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusCreated)
|
|
mustEncodeTestJSON(w, map[string]interface{}{"success": true})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "app1"
|
|
|
|
result, err := p.RegisterWebhook(context.Background(), "tok", "pg1")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, result, "pg1")
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — ExchangeToken (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_ExchangeToken_NoAppID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
p.appID = ""
|
|
p.appSecret = ""
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "App ID")
|
|
}
|
|
|
|
func TestInstagramProvider_ExchangeToken_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"access_token": "token123",
|
|
"expires_in": 3600,
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
result, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "token123", result.AccessToken)
|
|
}
|
|
|
|
func TestInstagramProvider_ExchangeToken_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 400")
|
|
}
|
|
|
|
func TestInstagramProvider_ExchangeToken_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
srv.Close() // close immediately to simulate network error
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — SendMessage (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_SendMessage_NoIGID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "non_numeric"}
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
|
|
_, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "recipient IGID")
|
|
}
|
|
|
|
func TestInstagramProvider_SendMessage_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "12345"}
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: ""}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
|
|
_, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "access token")
|
|
}
|
|
|
|
func TestInstagramProvider_SendMessage_TextSuccess_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"recipient_id": "12345",
|
|
"message_id": "mid_abc",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "12345"}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
|
|
result, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mid_abc", result.ExternalID)
|
|
}
|
|
|
|
func TestInstagramProvider_SendMessage_AttachmentSuccess_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"recipient_id": "12345",
|
|
"message_id": "mid_att",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "12345"}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
msg := &model.Message{Content: "https://img.url/x.png", ContentType: "image"}
|
|
|
|
result, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mid_att", result.ExternalID)
|
|
}
|
|
|
|
func TestInstagramProvider_SendMessage_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "12345"}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
|
|
_, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 401")
|
|
}
|
|
|
|
func TestInstagramProvider_SendMessage_InputText_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"message_id": "mid_input",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
contact := &model.Contact{Base: model.Base{ID: 1}, Identifier: "99999"}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
msg := &model.Message{Content: "test", ContentType: "input_text"}
|
|
|
|
result, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mid_input", result.ExternalID)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — GetContactProfile (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_GetContactProfile_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: ""}
|
|
_, err := p.GetContactProfile(context.Background(), inbox, "igid123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "access token")
|
|
}
|
|
|
|
func TestInstagramProvider_GetContactProfile_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "igid123",
|
|
"username": "testuser",
|
|
"name": "Test User",
|
|
"profile_picture_url": "https://pic.url/me.jpg",
|
|
"biography": "bio text",
|
|
"followers_count": 500,
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
|
|
profile, err := p.GetContactProfile(context.Background(), inbox, "igid123")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "testuser", profile.Name)
|
|
assert.Equal(t, "https://pic.url/me.jpg", profile.AvatarURL)
|
|
}
|
|
|
|
func TestInstagramProvider_GetContactProfile_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
|
|
_, err := p.GetContactProfile(context.Background(), inbox, "igid123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 404")
|
|
}
|
|
|
|
func TestInstagramProvider_GetContactProfile_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
|
|
_, err := p.GetContactProfile(context.Background(), inbox, "igid123")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — ListWebhooks / DeleteWebhook (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_ListWebhooks_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodGet, r.Method)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`[{"id":"app1","name":"Test App"}]`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
result, err := p.ListWebhooks(context.Background(), "tok", "page1")
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, "app1", result[0]["id"])
|
|
}
|
|
|
|
func TestInstagramProvider_ListWebhooks_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.ListWebhooks(context.Background(), "tok", "page1")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 403")
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteWebhook_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodDelete, r.Method)
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.DeleteWebhook(context.Background(), "tok", "page1")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteWebhook_StatusNoContent_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.DeleteWebhook(context.Background(), "tok", "page1")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteWebhook_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.DeleteWebhook(context.Background(), "tok", "page1")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "status 500")
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteWebhook_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.DeleteWebhook(context.Background(), "tok", "page1")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — RefreshToken / CheckAuthorizationError / OnReauthorization (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_RefreshToken_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no current")
|
|
}
|
|
|
|
func TestInstagramProvider_RefreshToken_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"access_token": "new_tok",
|
|
"expires_in": 5000,
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
result, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{
|
|
"page_access_token": "old_tok",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "new_tok", result.AccessToken)
|
|
}
|
|
|
|
func TestInstagramProvider_RefreshToken_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{
|
|
"page_access_token": "old_tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 401")
|
|
}
|
|
|
|
func TestInstagramProvider_CheckAuthError_Code190_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.True(t, p.CheckAuthorizationError(context.Background(), fmt.Errorf("OAuthException code 190")))
|
|
}
|
|
|
|
func TestInstagramProvider_CheckAuthError_Subcode463_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.True(t, p.CheckAuthorizationError(context.Background(), fmt.Errorf("error_subcode 463")))
|
|
}
|
|
|
|
func TestInstagramProvider_CheckAuthError_Subcode467_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.True(t, p.CheckAuthorizationError(context.Background(), fmt.Errorf("error_subcode 467")))
|
|
}
|
|
|
|
func TestInstagramProvider_CheckAuthError_Nil_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.False(t, p.CheckAuthorizationError(context.Background(), nil))
|
|
}
|
|
|
|
func TestInstagramProvider_CheckAuthError_NotAuth_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.False(t, p.CheckAuthorizationError(context.Background(), fmt.Errorf("some other error")))
|
|
}
|
|
|
|
func TestInstagramProvider_OnReauthorization_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
err := p.OnReauthorization(context.Background(), &model.Inbox{Base: model.Base{ID: 1}})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — ProcessCommentIncoming (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_ProcessCommentIncoming_Nil_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ProcessCommentIncoming(context.Background(), &model.Inbox{}, nil, EventIGComment)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "nil")
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessCommentIncoming_Comment_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
comment := &IGCommentChangeValue{
|
|
CommentID: "c1",
|
|
MediaID: "m1",
|
|
Text: "nice post",
|
|
From: IGCommentFrom{ID: "u1", Username: "user1"},
|
|
}
|
|
msg, err := p.ProcessCommentIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, comment, EventIGComment)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "nice post", msg.Content)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessCommentIncoming_Reply_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
comment := &IGCommentChangeValue{
|
|
CommentID: "c2",
|
|
ParentID: "c1",
|
|
MediaID: "m1",
|
|
Text: "reply text",
|
|
From: IGCommentFrom{ID: "u2", Username: "user2"},
|
|
}
|
|
msg, err := p.ProcessCommentIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, comment, EventIGCommentReply)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "reply text", msg.Content)
|
|
assert.Equal(t, "c1", msg.Extra["parent_id"])
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessCommentIncoming_Deleted_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
comment := &IGCommentChangeValue{
|
|
CommentID: "c3",
|
|
MediaID: "m1",
|
|
IsDeleted: true,
|
|
From: IGCommentFrom{ID: "u3"},
|
|
}
|
|
msg, err := p.ProcessCommentIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, comment, EventIGCommentDeleted)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, msg.Content, "deleted")
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — Comment API methods (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_GetComments_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.GetComments(context.Background(), "", "m1", 0, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_GetComments_NoMediaID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.GetComments(context.Background(), "tok", "", 0, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_GetComments_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Contains(t, r.URL.Path, "comments")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{"data":[{"id":"c1","text":"hello"}]}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
resp, err := p.GetComments(context.Background(), "tok", "m1", 10, "cursor1")
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp.Data, 1)
|
|
}
|
|
|
|
func TestInstagramProvider_GetComments_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
_, err := p.GetComments(context.Background(), "tok", "m1", 0, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_GetCommentReplies_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.GetCommentReplies(context.Background(), "", "c1", 0, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_GetCommentReplies_NoCommentID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.GetCommentReplies(context.Background(), "tok", "", 0, "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_GetCommentReplies_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Contains(t, r.URL.Path, "replies")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{"data":[{"id":"r1","text":"reply"}]}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
resp, err := p.GetCommentReplies(context.Background(), "tok", "c1", 5, "")
|
|
require.NoError(t, err)
|
|
assert.Len(t, resp.Data, 1)
|
|
}
|
|
|
|
func TestInstagramProvider_ReplyToComment_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ReplyToComment(context.Background(), "", "c1", "message")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ReplyToComment_NoCommentID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ReplyToComment(context.Background(), "tok", "", "message")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ReplyToComment_NoMessage_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ReplyToComment(context.Background(), "tok", "c1", "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ReplyToComment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{"id":"r1"}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
resp, err := p.ReplyToComment(context.Background(), "tok", "c1", "hello")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "r1", resp.ID)
|
|
}
|
|
|
|
func TestInstagramProvider_HideComment_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.HideComment(context.Background(), "", "c1", true)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_HideComment_NoCommentID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.HideComment(context.Background(), "tok", "", true)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_HideComment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{"success":true}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
resp, err := p.HideComment(context.Background(), "tok", "c1", true)
|
|
require.NoError(t, err)
|
|
assert.True(t, resp.Success)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteComment_NoToken_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.DeleteComment(context.Background(), "", "c1")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteComment_NoCommentID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.DeleteComment(context.Background(), "tok", "")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteComment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Equal(t, http.MethodDelete, r.Method)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{"success":true}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
resp, err := p.DeleteComment(context.Background(), "tok", "c1")
|
|
require.NoError(t, err)
|
|
assert.True(t, resp.Success)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — SendTypingOn/Off, processIGAttachments (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_SendTypingOn_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOn(context.Background(), "igid", "tok")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_SendTypingOff_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOff(context.Background(), "igid", "tok")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_SendTypingOn_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOn(context.Background(), "igid", "tok")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 403")
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIGAttachments_AllTypes_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
atts := []FBAttachment{
|
|
{Type: "image", Payload: FBAttachmentPayload{URL: "http://img"}},
|
|
{Type: "video", Payload: FBAttachmentPayload{URL: "http://vid"}},
|
|
{Type: "audio", Payload: FBAttachmentPayload{URL: "http://aud"}},
|
|
{Type: "file", Payload: FBAttachmentPayload{URL: "http://file"}},
|
|
{Type: "animated_media", Payload: FBAttachmentPayload{URL: "http://anim"}},
|
|
{Type: "story_mention", Payload: FBAttachmentPayload{URL: "http://story"}},
|
|
{Type: "share", Payload: FBAttachmentPayload{URL: "http://share"}},
|
|
{Type: "location", Payload: FBAttachmentPayload{URL: "http://loc"}},
|
|
{Type: "unknown_type", Payload: FBAttachmentPayload{URL: "http://unk"}},
|
|
}
|
|
result := p.processIGAttachments(atts)
|
|
assert.Len(t, result, 9)
|
|
}
|
|
|
|
func TestInstagramProvider_ExtractRecipientIGID_Numeric_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.Equal(t, "12345", p.extractRecipientIGID(&model.Contact{Identifier: "12345"}))
|
|
}
|
|
|
|
func TestInstagramProvider_ExtractRecipientIGID_NonNumeric_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.Equal(t, "", p.extractRecipientIGID(&model.Contact{Identifier: "abc"}))
|
|
}
|
|
|
|
func TestInstagramProvider_ExtractRecipientIGID_Empty_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.Equal(t, "", p.extractRecipientIGID(&model.Contact{Identifier: ""}))
|
|
}
|
|
|
|
func TestInstagramProvider_MapContentType_All_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
assert.Equal(t, "image", p.mapContentTypeToIGAttachmentType("image"))
|
|
assert.Equal(t, "video", p.mapContentTypeToIGAttachmentType("video"))
|
|
assert.Equal(t, "audio", p.mapContentTypeToIGAttachmentType("audio"))
|
|
assert.Equal(t, "file", p.mapContentTypeToIGAttachmentType("file"))
|
|
assert.Equal(t, "file", p.mapContentTypeToIGAttachmentType("unknown"))
|
|
}
|
|
|
|
func TestInstagramProvider_GetPageAccessTokenFromInbox_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok123"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
assert.Equal(t, "tok123", p.GetPageAccessTokenFromInbox(inbox))
|
|
}
|
|
|
|
func TestInstagramProvider_GetPageAccessTokenFromInbox_Empty_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: ""}
|
|
assert.Equal(t, "", p.GetPageAccessTokenFromInbox(inbox))
|
|
}
|
|
|
|
func TestInstagramProvider_HandleWebhook_NonInstagram_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
err := p.HandleWebhook(context.Background(), map[string]interface{}{
|
|
"object": "page",
|
|
"entry": []interface{}{},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_HandleWebhook_Instagram_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
err := p.HandleWebhook(context.Background(), map[string]interface{}{
|
|
"object": "instagram",
|
|
"entry": []interface{}{},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncomingMessage_NilPayload_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ProcessIncomingMessage(context.Background(), &model.Inbox{}, map[string]interface{}{
|
|
"object": "not_instagram",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_BadJSON_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
_, err := p.ProcessIncoming(context.Background(), &model.Inbox{}, []byte("bad json"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "parse")
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_NotInstagram_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(map[string]interface{}{
|
|
"object": "page",
|
|
"entry": []interface{}{},
|
|
})
|
|
_, err := p.ProcessIncoming(context.Background(), &model.Inbox{}, payload)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "instagram")
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_EchoMessage_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "echo", IsEcho: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
_, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no processable")
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_TextMessage_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "hello"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello", msg.Content)
|
|
assert.Equal(t, channel.ChannelInstagram, msg.ChannelType)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_WithAttachments_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Text: "with img",
|
|
Attachments: []FBAttachment{
|
|
{Type: "image", Payload: FBAttachmentPayload{URL: "http://img.url"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_Postback_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Postback: &FBPostBackData{Title: "Get Started", Payload: "START"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "START", msg.Content)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_ReplyTo_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Text: "reply",
|
|
ReplyTo: &FBReplyTo{Mid: "orig_mid"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "orig_mid", msg.ReplyToID)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_CommentChange_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c1",
|
|
"text": "comment text",
|
|
"timestamp": "2024-01-01T00:00:00+00:00",
|
|
"from": map[string]interface{}{"id": "u1", "username": "user1"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "comment text", msg.Content)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_CommentReplyChange_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c2",
|
|
"parent_id": "c1",
|
|
"text": "reply comment",
|
|
"from": map[string]interface{}{"id": "u2"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "reply comment", msg.Content)
|
|
}
|
|
|
|
func TestInstagramProvider_ProcessIncoming_CommentDeletedChange_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c3",
|
|
"is_deleted": true,
|
|
"from": map[string]interface{}{"id": "u3"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, msg.Content, "deleted")
|
|
}
|
|
|
|
func TestInstagramProvider_OnCreate_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if strings.Contains(r.URL.Path, "instagram_business_account") || strings.Contains(r.URL.RawQuery, "instagram_business_account") {
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"instagram_business_account": map[string]interface{}{"id": "ig_biz_1"},
|
|
})
|
|
} else {
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "ig1",
|
|
"username": "iguser",
|
|
"name": "IG User",
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
config := channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
"connected_fb_page_id": "page1",
|
|
}
|
|
result, err := p.OnCreate(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "iguser", result["instagram_account_name"])
|
|
}
|
|
|
|
func TestInstagramProvider_OnDestroy_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
config := channel.ChannelConfig{
|
|
"connected_fb_page_id": "page1",
|
|
"page_access_token": "tok",
|
|
}
|
|
err := p.OnDestroy(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_OnDestroy_NoAppID_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
p.appID = ""
|
|
config := channel.ChannelConfig{
|
|
"connected_fb_page_id": "page1",
|
|
"page_access_token": "tok",
|
|
}
|
|
err := p.OnDestroy(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_CreateChannel_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if strings.Contains(r.URL.RawQuery, "instagram_business_account") {
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"instagram_business_account": map[string]interface{}{"id": "ig_biz_1"},
|
|
})
|
|
} else {
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "ig1",
|
|
"username": "iguser",
|
|
})
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
ch, err := p.CreateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
"connected_fb_page_id": "page1",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, ch)
|
|
}
|
|
|
|
func TestInstagramProvider_CreateChannel_APIError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.CreateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_UpdateChannel_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
ch, err := p.UpdateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"page_access_token": "newtok",
|
|
"instagram_account_name": "newname",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, ch)
|
|
}
|
|
|
|
func TestInstagramProvider_DeleteChannel_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
err := p.DeleteChannel(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_GET_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"webhook_verify_token": "mytoken"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "subscribe", "hub.verify_token": "mytoken"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_GET_BadMode_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "not_subscribe"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "hub.mode")
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_GET_TokenMismatch_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"webhook_verify_token": "correct"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "subscribe", "hub.verify_token": "wrong"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "mismatch")
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_POST_NoSignature_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "missing")
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_POST_NoSecret_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
p.appSecret = ""
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": "sha256=abc"},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err) // skips verification when no secret
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_POST_BadSig_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
p.appSecret = "secret"
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": "sha256=bad"},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "signature")
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateWebhookRequest_POST_ValidSig_Cov5(t *testing.T) {
|
|
p := NewInstagramProvider()
|
|
p.appSecret = "secret"
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
|
|
body := []byte(`{"object":"instagram"}`)
|
|
mac := hmacSha256Hex(p.appSecret, body)
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": mac},
|
|
Body: body,
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// User Mapping — shouldUpdateProfile, truncateID (Cov5)
|
|
// ===========================
|
|
|
|
func TestShouldUpdateProfile_EmptyName_Cov5(t *testing.T) {
|
|
c := &model.Contact{Name: ""}
|
|
assert.True(t, shouldUpdateProfile(c))
|
|
}
|
|
|
|
func TestShouldUpdateProfile_FBUserPrefix_Cov5(t *testing.T) {
|
|
c := &model.Contact{Name: "FB User 12345678", Base: model.Base{UpdatedAt: time.Now()}}
|
|
assert.True(t, shouldUpdateProfile(c))
|
|
}
|
|
|
|
func TestShouldUpdateProfile_IGUserPrefix_Cov5(t *testing.T) {
|
|
c := &model.Contact{Name: "IG User 12345678", Base: model.Base{UpdatedAt: time.Now()}}
|
|
assert.True(t, shouldUpdateProfile(c))
|
|
}
|
|
|
|
func TestShouldUpdateProfile_Stale_Cov5(t *testing.T) {
|
|
c := &model.Contact{Name: "John Doe", Base: model.Base{UpdatedAt: time.Now().Add(-48 * time.Hour)}}
|
|
assert.True(t, shouldUpdateProfile(c))
|
|
}
|
|
|
|
func TestShouldUpdateProfile_NotStale_Cov5(t *testing.T) {
|
|
c := &model.Contact{Name: "John Doe", Base: model.Base{UpdatedAt: time.Now()}}
|
|
assert.False(t, shouldUpdateProfile(c))
|
|
}
|
|
|
|
func TestTruncateID_Long_Cov5(t *testing.T) {
|
|
result := truncateID("1234567890abcdef")
|
|
assert.Len(t, result, 8)
|
|
assert.Equal(t, "90abcdef", result)
|
|
}
|
|
|
|
func TestTruncateID_Short_Cov5(t *testing.T) {
|
|
result := truncateID("abc")
|
|
assert.Equal(t, "abc", result)
|
|
}
|
|
|
|
func TestTruncateID_Empty_Cov5(t *testing.T) {
|
|
result := truncateID("")
|
|
assert.Equal(t, "", result)
|
|
}
|
|
|
|
// ===========================
|
|
// Media — DownloadAttachment, UploadAttachment (Cov5)
|
|
// ===========================
|
|
|
|
func TestMediaService_DownloadAttachment_EmptyURL_Cov5(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ms := NewMediaService("https://graph", dir)
|
|
_, err := ms.DownloadAttachment(context.Background(), "", "file.txt")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "empty")
|
|
}
|
|
|
|
func TestMediaService_DownloadAttachment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := w.Write([]byte("file content")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
path, err := ms.DownloadAttachment(context.Background(), srv.URL+"/file", "test.txt")
|
|
require.NoError(t, err)
|
|
assert.FileExists(t, path)
|
|
|
|
content, _ := os.ReadFile(path)
|
|
assert.Equal(t, "file content", string(content))
|
|
}
|
|
|
|
func TestMediaService_DownloadAttachment_AutoFilename_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := w.Write([]byte("content")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
path, err := ms.DownloadAttachment(context.Background(), srv.URL+"/my_file.png", "")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, filepath.Base(path), "my_file.png")
|
|
}
|
|
|
|
func TestMediaService_DownloadAttachment_AlreadyDownloaded_Cov5(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Pre-create the file
|
|
existingPath := filepath.Join(dir, "cached.txt")
|
|
require.NoError(t, os.WriteFile(existingPath, []byte("cached"), 0644))
|
|
|
|
ms := NewMediaService("https://graph", dir)
|
|
path, err := ms.DownloadAttachment(context.Background(), "http://example.com/cached", "cached.txt")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, existingPath, path)
|
|
}
|
|
|
|
func TestMediaService_DownloadAttachment_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
_, err := ms.DownloadAttachment(context.Background(), srv.URL+"/missing", "file.txt")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 404")
|
|
}
|
|
|
|
func TestMediaService_UploadAttachment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"attachment_id": "att_123"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "upload.txt")
|
|
require.NoError(t, os.WriteFile(filePath, []byte("upload content"), 0644))
|
|
|
|
ms := NewMediaService(srv.URL, dir)
|
|
attID, err := ms.UploadAttachment(context.Background(), "tok", filePath, "file")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "att_123", attID)
|
|
}
|
|
|
|
func TestMediaService_UploadAttachment_FileNotFound_Cov5(t *testing.T) {
|
|
dir := t.TempDir()
|
|
ms := NewMediaService("https://graph", dir)
|
|
_, err := ms.UploadAttachment(context.Background(), "tok", filepath.Join(dir, "nonexistent.txt"), "file")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "read file")
|
|
}
|
|
|
|
func TestMediaService_UploadAttachment_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "upload.txt")
|
|
require.NoError(t, os.WriteFile(filePath, []byte("content"), 0644))
|
|
|
|
ms := NewMediaService(srv.URL, dir)
|
|
_, err := ms.UploadAttachment(context.Background(), "tok", filePath, "file")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 400")
|
|
}
|
|
|
|
func TestMediaService_UploadAttachment_NoAttachmentID_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if _, err := w.Write([]byte(`{}`)); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
filePath := filepath.Join(dir, "upload.txt")
|
|
require.NoError(t, os.WriteFile(filePath, []byte("content"), 0644))
|
|
|
|
ms := NewMediaService(srv.URL, dir)
|
|
_, err := ms.UploadAttachment(context.Background(), "tok", filePath, "file")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "attachment_id")
|
|
}
|
|
|
|
func TestMediaService_SendAttachmentWithURL_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ms := NewMediaService(srv.URL, t.TempDir())
|
|
err := ms.SendAttachmentWithURL(context.Background(), "tok", "recip", "image", "http://img")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestMediaService_SendAttachmentWithURL_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ms := NewMediaService(srv.URL, t.TempDir())
|
|
err := ms.SendAttachmentWithURL(context.Background(), "tok", "recip", "image", "http://img")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestMediaService_SendAttachmentWithID_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ms := NewMediaService(srv.URL, t.TempDir())
|
|
err := ms.SendAttachmentWithID(context.Background(), "tok", "recip", "image", "att_123")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestMediaService_SendAttachmentWithID_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
ms := NewMediaService(srv.URL, t.TempDir())
|
|
err := ms.SendAttachmentWithID(context.Background(), "tok", "recip", "image", "att_123")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestMediaService_DownloadFBAttachment_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := w.Write([]byte("image data")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
fbAtt := FBAttachment{
|
|
Type: "image",
|
|
Payload: FBAttachmentPayload{URL: srv.URL + "/img.png", Name: "img.png"},
|
|
}
|
|
att, err := ms.DownloadFBAttachment(context.Background(), fbAtt, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, att)
|
|
assert.Equal(t, "image/jpeg", att.ContentType)
|
|
assert.Equal(t, "img.png", att.Filename)
|
|
}
|
|
|
|
func TestMediaService_DownloadFBAttachment_NoURL_Cov5(t *testing.T) {
|
|
ms := NewMediaService("https://graph", t.TempDir())
|
|
fbAtt := FBAttachment{
|
|
Type: "image",
|
|
Payload: FBAttachmentPayload{},
|
|
}
|
|
_, err := ms.DownloadFBAttachment(context.Background(), fbAtt, channel.ChannelFacebook)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no download URL")
|
|
}
|
|
|
|
func TestMediaService_DownloadFBAttachment_SrcFallback_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := w.Write([]byte("sticker")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
fbAtt := FBAttachment{
|
|
Type: "image",
|
|
Payload: FBAttachmentPayload{Src: srv.URL + "/sticker"},
|
|
}
|
|
att, err := ms.DownloadFBAttachment(context.Background(), fbAtt, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, att)
|
|
}
|
|
|
|
func TestMediaService_DownloadFBAttachment_Location_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if _, err := w.Write([]byte("data")); err != nil {
|
|
panic(err)
|
|
}
|
|
}))
|
|
defer srv.Close()
|
|
|
|
dir := t.TempDir()
|
|
ms := NewMediaService(srv.URL, dir)
|
|
fbAtt := FBAttachment{
|
|
Type: "location",
|
|
Payload: FBAttachmentPayload{
|
|
URL: srv.URL + "/loc",
|
|
Coordinates: &FBLocationCoordinates{Lat: 37.77, Long: -122.41},
|
|
},
|
|
}
|
|
att, err := ms.DownloadFBAttachment(context.Background(), fbAtt, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, att)
|
|
assert.Equal(t, 37.77, att.Extra["latitude"])
|
|
}
|
|
|
|
// ===========================
|
|
// Media — helper functions (Cov5)
|
|
// ===========================
|
|
|
|
func TestGenerateFilenameFromURL_WithQuery_Cov5(t *testing.T) {
|
|
result := generateFilenameFromURL("http://example.com/path/file.png?token=abc")
|
|
assert.Equal(t, "file.png", result)
|
|
}
|
|
|
|
func TestGenerateFilenameFromURL_NoPath_Cov5(t *testing.T) {
|
|
result := generateFilenameFromURL("http://example.com")
|
|
assert.NotEmpty(t, result)
|
|
}
|
|
|
|
func TestGetFileSize_Nonexistent_Cov5(t *testing.T) {
|
|
result := getFileSize("/nonexistent/path/file.txt")
|
|
assert.Equal(t, int64(0), result)
|
|
}
|
|
|
|
func TestGetFileSize_Existing_Cov5(t *testing.T) {
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "f.txt")
|
|
require.NoError(t, os.WriteFile(p, []byte("12345"), 0644))
|
|
result := getFileSize(p)
|
|
assert.Equal(t, int64(5), result)
|
|
}
|
|
|
|
func TestParseJSONResponse_Valid_Cov5(t *testing.T) {
|
|
var target FBAttachmentUploadResponse
|
|
err := parseJSONResponse([]byte(`{"attachment_id":"att1"}`), &target)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "att1", target.AttachmentID)
|
|
}
|
|
|
|
func TestParseJSONResponse_Invalid_Cov5(t *testing.T) {
|
|
var target FBAttachmentUploadResponse
|
|
err := parseJSONResponse([]byte(`invalid`), &target)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestMimeTypeFromFBType_All_Cov5(t *testing.T) {
|
|
assert.Equal(t, "image/jpeg", mimeTypeFromFBType("image"))
|
|
assert.Equal(t, "video/mp4", mimeTypeFromFBType("video"))
|
|
assert.Equal(t, "audio/mp4", mimeTypeFromFBType("audio"))
|
|
assert.Equal(t, "application/octet-stream", mimeTypeFromFBType("file"))
|
|
assert.Equal(t, "application/json", mimeTypeFromFBType("location"))
|
|
assert.Equal(t, "application/json", mimeTypeFromFBType("template"))
|
|
assert.Equal(t, "application/octet-stream", mimeTypeFromFBType("fallback"))
|
|
assert.Equal(t, "application/octet-stream", mimeTypeFromFBType("unknown"))
|
|
}
|
|
|
|
func TestDetermineFBAttachmentType_All_Cov5(t *testing.T) {
|
|
assert.Equal(t, "image", DetermineFBAttachmentType(channel.ContentImage))
|
|
assert.Equal(t, "video", DetermineFBAttachmentType(channel.ContentVideo))
|
|
assert.Equal(t, "audio", DetermineFBAttachmentType(channel.ContentAudio))
|
|
assert.Equal(t, "file", DetermineFBAttachmentType(channel.ContentFile))
|
|
assert.Equal(t, "location", DetermineFBAttachmentType(channel.ContentLocation))
|
|
assert.Equal(t, "file", DetermineFBAttachmentType(channel.ContentText))
|
|
}
|
|
|
|
// ===========================
|
|
// Pipeline — ProcessFacebookEvent / ProcessInstagramEvent (Cov5)
|
|
// ===========================
|
|
|
|
func TestPipeline_ProcessFacebookEvent_BadJSON_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessFacebookEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, []byte("bad json"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "transform failed")
|
|
}
|
|
|
|
func TestPipeline_ProcessInstagramEvent_BadJSON_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessInstagramEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, []byte("bad json"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "transform failed")
|
|
}
|
|
|
|
func TestPipeline_ProcessFacebookEvent_NotPage_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
payload, _ := json.Marshal(map[string]interface{}{"object": "instagram"})
|
|
err := proc.ProcessFacebookEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessInstagramEvent_NotInstagram_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
payload, _ := json.Marshal(map[string]interface{}{"object": "page"})
|
|
err := proc.ProcessInstagramEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessFacebookEvent_EchoMessage_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "echo", IsEcho: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
err := proc.ProcessFacebookEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.Error(t, err) // echo is skipped, then "no processable" error
|
|
}
|
|
|
|
func TestPipeline_ProcessInstagramEvent_EchoMessage_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "echo", IsEcho: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
err := proc.ProcessInstagramEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessDeliveryReceipt_NilDelivery_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessDeliveryReceipt(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, &FBMessagingEvent{})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessDeliveryReceipt_WithDelivery_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessDeliveryReceipt(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, &FBMessagingEvent{
|
|
Delivery: &FBDeliveryData{Mids: []string{"m1"}, Watermark: 123},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessReadReceipt_NilRead_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessReadReceipt(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, &FBMessagingEvent{})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessReadReceipt_WithRead_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessReadReceipt(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, &FBMessagingEvent{
|
|
Read: &FBReadData{Watermark: 123},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestPipeline_ProcessInstagramCommentEvent_NilComment_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
err := proc.ProcessInstagramCommentEvent(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, &ParsedWebhookEvent{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no comment data")
|
|
}
|
|
|
|
func TestPipeline_NewIncomingProcessor_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewIncomingProcessor(fbProv, igProv, nil, nil)
|
|
assert.NotNil(t, proc)
|
|
assert.Equal(t, fbProv, proc.fbProvider)
|
|
assert.Equal(t, igProv, proc.igProvider)
|
|
}
|
|
|
|
func TestPipeline_NewOutgoingProcessor_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewOutgoingProcessor(fbProv, igProv, nil)
|
|
assert.NotNil(t, proc)
|
|
}
|
|
|
|
func TestPipeline_Outgoing_UnsupportedType_Cov5(t *testing.T) {
|
|
fbProv := NewFacebookProvider()
|
|
igProv := NewInstagramProvider()
|
|
proc := NewOutgoingProcessor(fbProv, igProv, nil)
|
|
_, err := proc.ProcessOutgoingMessage(context.Background(), &model.Inbox{ChannelType: "unknown"}, &model.Message{}, &model.Contact{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unsupported")
|
|
}
|
|
|
|
func TestPipeline_ExtractPageAccessTokenFromInbox_Success_Cov5(t *testing.T) {
|
|
s := &Service{}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok123"})
|
|
inbox := &model.Inbox{ChannelConfig: string(configJSON)}
|
|
token := s.extractPageAccessTokenFromInbox(inbox)
|
|
assert.Equal(t, "tok123", token)
|
|
}
|
|
|
|
func TestPipeline_ExtractPageAccessTokenFromInbox_Empty_Cov5(t *testing.T) {
|
|
s := &Service{}
|
|
inbox := &model.Inbox{ChannelConfig: ""}
|
|
token := s.extractPageAccessTokenFromInbox(inbox)
|
|
assert.Equal(t, "", token)
|
|
}
|
|
|
|
func TestPipeline_ExtractPageAccessTokenFromInbox_BadJSON_Cov5(t *testing.T) {
|
|
s := &Service{}
|
|
inbox := &model.Inbox{ChannelConfig: "bad json"}
|
|
token := s.extractPageAccessTokenFromInbox(inbox)
|
|
assert.Equal(t, "", token)
|
|
}
|
|
|
|
// ===========================
|
|
// FacebookProvider — TakeThreadControl, SendTyping, MarkSeen (Cov5)
|
|
// ===========================
|
|
|
|
func TestFBProvider_TakeThreadControl_EmptyPSID_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.TakeThreadControl(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, "")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "PSID")
|
|
}
|
|
|
|
func TestFBProvider_TakeThreadControl_NoToken_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.TakeThreadControl(context.Background(), &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: ""}, "psid123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "access token")
|
|
}
|
|
|
|
func TestFBProvider_TakeThreadControl_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
assert.Contains(t, r.URL.Path, "take_thread_control")
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
err := p.TakeThreadControl(context.Background(), inbox, "psid123")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_TakeThreadControl_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusForbidden)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
err := p.TakeThreadControl(context.Background(), inbox, "psid123")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 403")
|
|
}
|
|
|
|
func TestFBProvider_SendTypingOn_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOn(context.Background(), "psid", "tok")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_SendTypingOff_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOff(context.Background(), "psid", "tok")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_MarkSeen_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.MarkSeen(context.Background(), "psid", "tok")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_SendTypingOn_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
err := p.SendTypingOn(context.Background(), "psid", "tok")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 401")
|
|
}
|
|
|
|
func TestFBProvider_ExtractRecipientPSID_Numeric_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
assert.Equal(t, "12345", p.extractRecipientPSID(&model.Contact{Identifier: "12345"}))
|
|
}
|
|
|
|
func TestFBProvider_ExtractRecipientPSID_NonNumeric_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
assert.Equal(t, "", p.extractRecipientPSID(&model.Contact{Identifier: "abc"}))
|
|
}
|
|
|
|
func TestFBProvider_GetPageAccessTokenFromInbox_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok456"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
assert.Equal(t, "tok456", p.GetPageAccessTokenFromInbox(inbox))
|
|
}
|
|
|
|
func TestFBProvider_HandleWebhook_NonPage_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.HandleWebhook(context.Background(), map[string]interface{}{
|
|
"object": "instagram",
|
|
"entry": []interface{}{},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_HandleWebhook_Page_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.HandleWebhook(context.Background(), map[string]interface{}{
|
|
"object": "page",
|
|
"entry": []interface{}{},
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ExchangeLongLivedUserToken_Empty_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
_, err := p.ExchangeLongLivedUserToken(context.Background(), " ")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestFBProvider_ExchangeLongLivedUserToken_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"access_token": "long_lived"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
token, err := p.ExchangeLongLivedUserToken(context.Background(), "short")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "long_lived", token)
|
|
}
|
|
|
|
func TestFBProvider_ExchangeLongLivedUserToken_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
_, err := p.ExchangeLongLivedUserToken(context.Background(), "short")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ExchangeLongLivedUserToken_EmptyToken_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
_, err := p.ExchangeLongLivedUserToken(context.Background(), "short")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "empty")
|
|
}
|
|
|
|
func TestFBProvider_ListFacebookPages_Empty_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
_, err := p.ListFacebookPages(context.Background(), " ")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "required")
|
|
}
|
|
|
|
func TestFBProvider_ListFacebookPages_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"data": []map[string]interface{}{
|
|
{"id": "page1", "name": "Page One"},
|
|
},
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
pages, err := p.ListFacebookPages(context.Background(), "user_token")
|
|
require.NoError(t, err)
|
|
assert.Len(t, pages, 1)
|
|
assert.Equal(t, "Page One", pages[0].Name)
|
|
}
|
|
|
|
func TestFBProvider_FetchInstagramBusinessAccountID_Empty_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
id, err := p.FetchInstagramBusinessAccountID(context.Background(), " ")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "", id)
|
|
}
|
|
|
|
func TestFBProvider_FetchInstagramBusinessAccountID_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"instagram_business_account": map[string]interface{}{"id": "ig_biz_1"},
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
id, err := p.FetchInstagramBusinessAccountID(context.Background(), "tok")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ig_biz_1", id)
|
|
}
|
|
|
|
func TestFBProvider_FetchInstagramBusinessAccountID_NoIG_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"id": "page1"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
id, err := p.FetchInstagramBusinessAccountID(context.Background(), "tok")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "", id)
|
|
}
|
|
|
|
func TestFBProvider_GetContactProfile_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "psid1",
|
|
"name": "FB User",
|
|
"first_name": "FB",
|
|
"last_name": "User",
|
|
"profile_pic": "http://pic",
|
|
"locale": "en_US",
|
|
"timezone": 5,
|
|
"gender": "male",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
|
|
profile, err := p.GetContactProfile(context.Background(), inbox, "psid1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "FB User", profile.Name)
|
|
assert.Equal(t, "http://pic", profile.AvatarURL)
|
|
}
|
|
|
|
func TestFBProvider_GetContactProfile_NoToken_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
_, err := p.GetContactProfile(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, "psid1")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "access token")
|
|
}
|
|
|
|
func TestFBProvider_GetContactProfile_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
|
|
_, err := p.GetContactProfile(context.Background(), inbox, "psid1")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 404")
|
|
}
|
|
|
|
func TestFBProvider_SendMessage_NoPSID_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
contact := &model.Contact{Identifier: "non_numeric"}
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
_, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "PSID")
|
|
}
|
|
|
|
func TestFBProvider_SendMessage_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"recipient_id": "12345",
|
|
"message_id": "mid_fb",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
contact := &model.Contact{Identifier: "12345"}
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"page_access_token": "tok"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
msg := &model.Message{Content: "hello", ContentType: "text"}
|
|
|
|
result, err := p.SendMessage(context.Background(), inbox, msg, contact)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "mid_fb", result.ExternalID)
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_TextMessage_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "hello FB"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello FB", msg.Content)
|
|
assert.Equal(t, channel.ChannelFacebook, msg.ChannelType)
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_BadJSON_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
_, err := p.ProcessIncoming(context.Background(), &model.Inbox{}, []byte("bad"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "parse")
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_NotPage_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload, _ := json.Marshal(map[string]interface{}{"object": "instagram"})
|
|
_, err := p.ProcessIncoming(context.Background(), &model.Inbox{}, payload)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "page")
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_WithAttachments_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Text: "with file",
|
|
Attachments: []FBAttachment{
|
|
{Type: "file", Payload: FBAttachmentPayload{URL: "http://file"}},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_Postback_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Postback: &FBPostBackData{Title: "Get Started", Payload: "START"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "START", msg.Content)
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncoming_Optin_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Optin: &FBOptinData{Ref: "m.me_ref"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
msg, err := p.ProcessIncoming(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "m.me_ref", msg.Content)
|
|
}
|
|
|
|
func TestFBProvider_ProcessFBAttachments_AllTypes_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
atts := []FBAttachment{
|
|
{Type: "image", Payload: FBAttachmentPayload{URL: "http://img"}},
|
|
{Type: "video", Payload: FBAttachmentPayload{URL: "http://vid"}},
|
|
{Type: "audio", Payload: FBAttachmentPayload{URL: "http://aud"}},
|
|
{Type: "file", Payload: FBAttachmentPayload{URL: "http://file"}},
|
|
{Type: "location", Payload: FBAttachmentPayload{URL: "http://loc"}},
|
|
{Type: "fallback", Payload: FBAttachmentPayload{Name: "fallback_name"}},
|
|
{Type: "unknown", Payload: FBAttachmentPayload{URL: "http://unk"}},
|
|
}
|
|
result := p.processFBAttachments(atts)
|
|
assert.Len(t, result, 7)
|
|
}
|
|
|
|
func TestFBProvider_CheckAuthError_Code190_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
assert.True(t, p.CheckAuthorizationError(context.Background(), fmt.Errorf("OAuthException code 190")))
|
|
}
|
|
|
|
func TestFBProvider_CheckAuthError_Nil_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
assert.False(t, p.CheckAuthorizationError(context.Background(), nil))
|
|
}
|
|
|
|
func TestFBProvider_OnReauthorization_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.OnReauthorization(context.Background(), &model.Inbox{Base: model.Base{ID: 1}})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_OnDestroy_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.OnDestroy(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// Webhook Parser — classifyMessagingEvent / classifyChangeEvent (Cov5)
|
|
// ===========================
|
|
|
|
func TestWebhookParser_ParsePayload_FBMessage_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "hello"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBMessage, events[0].EventType)
|
|
assert.Equal(t, "s1", events[0].SenderID)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_IGMessage_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Recipient: FBSenderRecipient{ID: "r1"},
|
|
Message: &FBMessageData{Mid: "m1", Text: "hello"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventIGMessage, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_BadJSON_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
_, err := wp.ParseWebhookPayload([]byte("bad json"))
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "parse")
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_Empty_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{Object: "page"})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, events)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_EchoMessage_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Message: &FBMessageData{Mid: "m1", IsEcho: true},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBMessageEcho, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_Postback_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Postback: &FBPostBackData{Title: "T", Payload: "P"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBPostback, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_Delivery_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Delivery: &FBDeliveryData{Mids: []string{"m1"}, Watermark: 100},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBDelivery, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_Read_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Read: &FBReadData{Watermark: 100},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBRead, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_Optin_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Optin: &FBOptinData{Ref: "ref1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBOptin, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_PassThreadControl_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
PassThreadControl: &FBThreadControlData{NewOwnerAppID: "app1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBPassThread, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_TakeThreadControl_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
TakeThreadControl: &FBThreadControlData{NewOwnerAppID: "app1"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventFBTakeThread, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_IGComment_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c1",
|
|
"text": "comment",
|
|
"timestamp": "2024-01-01T00:00:00Z",
|
|
"from": map[string]interface{}{"id": "u1", "username": "user1"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventIGComment, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_IGCommentReply_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c2",
|
|
"parent_id": "c1",
|
|
"text": "reply",
|
|
"from": map[string]interface{}{"id": "u2"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventIGCommentReply, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_IGCommentDeleted_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
commentVal := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c3",
|
|
"is_deleted": true,
|
|
"from": map[string]interface{}{"id": "u3"},
|
|
}
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "comments", Value: commentVal},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.Equal(t, EventIGCommentDeleted, events[0].EventType)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_PostbackWithReferral_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "page",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "page1",
|
|
Messaging: []FBMessagingEvent{
|
|
{
|
|
Sender: FBSenderRecipient{ID: "s1"},
|
|
Postback: &FBPostBackData{
|
|
Title: "T",
|
|
Payload: "P",
|
|
Referral: &FBReferralData{Ref: "ref1", Source: "SHORTLINK", Type: "OPEN_THREAD"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Len(t, events, 1)
|
|
assert.NotNil(t, events[0].Postback)
|
|
assert.NotNil(t, events[0].Postback.Referral)
|
|
assert.Equal(t, "ref1", events[0].Postback.Referral.Ref)
|
|
}
|
|
|
|
func TestWebhookParser_ParsePayload_UnhandledChange_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
payload, _ := json.Marshal(FBWebhookEvent{
|
|
Object: "instagram",
|
|
Entry: []FBWebhookEntry{
|
|
{
|
|
ID: "ig1",
|
|
Changes: []FBChangeEvent{
|
|
{Field: "unhandled_field", Value: map[string]interface{}{}},
|
|
},
|
|
},
|
|
},
|
|
})
|
|
events, err := wp.ParseWebhookPayload(payload)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, events)
|
|
}
|
|
|
|
// ===========================
|
|
// Webhook Parser — standalone helper functions (Cov5)
|
|
// ===========================
|
|
|
|
func TestIsIGCommentEvent_AllTypes_Cov5(t *testing.T) {
|
|
assert.True(t, IsIGCommentEvent(&ParsedWebhookEvent{EventType: EventIGComment}))
|
|
assert.True(t, IsIGCommentEvent(&ParsedWebhookEvent{EventType: EventIGCommentReply}))
|
|
assert.True(t, IsIGCommentEvent(&ParsedWebhookEvent{EventType: EventIGCommentDeleted}))
|
|
assert.False(t, IsIGCommentEvent(&ParsedWebhookEvent{EventType: EventFBMessage}))
|
|
}
|
|
|
|
func TestValidateWebhookSignature_EmptyHeader_Cov5(t *testing.T) {
|
|
assert.False(t, ValidateWebhookSignature("secret", "", []byte("body")))
|
|
}
|
|
|
|
func TestValidateWebhookSignature_BadFormat_Cov5(t *testing.T) {
|
|
assert.False(t, ValidateWebhookSignature("secret", "badformat", []byte("body")))
|
|
}
|
|
|
|
func TestValidateWebhookSignature_WrongAlgo_Cov5(t *testing.T) {
|
|
assert.False(t, ValidateWebhookSignature("secret", "md5=abc", []byte("body")))
|
|
}
|
|
|
|
func TestValidateWebhookSignature_Valid_Cov5(t *testing.T) {
|
|
body := []byte(`{"object":"page"}`)
|
|
sig := hmacSha256Hex("secret", body)
|
|
assert.True(t, ValidateWebhookSignature("secret", sig, body))
|
|
}
|
|
|
|
func TestValidateWebhookSignature_InvalidSig_Cov5(t *testing.T) {
|
|
assert.False(t, ValidateWebhookSignature("secret", "sha256=baadbaad", []byte("body")))
|
|
}
|
|
|
|
func TestVerifyWebhookChallenge_Success_Cov5(t *testing.T) {
|
|
challenge, ok := VerifyWebhookChallenge(map[string]string{
|
|
"hub.mode": "subscribe",
|
|
"hub.verify_token": "mytoken",
|
|
"hub.challenge": "challenge123",
|
|
}, "mytoken")
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "challenge123", challenge)
|
|
}
|
|
|
|
func TestVerifyWebhookChallenge_BadMode_Cov5(t *testing.T) {
|
|
_, ok := VerifyWebhookChallenge(map[string]string{
|
|
"hub.mode": "bad",
|
|
"hub.verify_token": "mytoken",
|
|
"hub.challenge": "challenge",
|
|
}, "mytoken")
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestVerifyWebhookChallenge_TokenMismatch_Cov5(t *testing.T) {
|
|
_, ok := VerifyWebhookChallenge(map[string]string{
|
|
"hub.mode": "subscribe",
|
|
"hub.verify_token": "wrong",
|
|
"hub.challenge": "challenge",
|
|
}, "correct")
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestIsEchoMessage_FBEcho_Cov5(t *testing.T) {
|
|
assert.True(t, IsEchoMessage(&ParsedWebhookEvent{EventType: EventFBMessageEcho}))
|
|
}
|
|
|
|
func TestIsEchoMessage_IGEcho_Cov5(t *testing.T) {
|
|
assert.True(t, IsEchoMessage(&ParsedWebhookEvent{EventType: EventIGMessageEcho}))
|
|
}
|
|
|
|
func TestIsEchoMessage_NotEcho_Cov5(t *testing.T) {
|
|
assert.False(t, IsEchoMessage(&ParsedWebhookEvent{EventType: EventFBMessage}))
|
|
}
|
|
|
|
func TestIsDeliveryOrReadReceipt_All_Cov5(t *testing.T) {
|
|
assert.True(t, IsDeliveryOrReadReceipt(&ParsedWebhookEvent{EventType: EventFBDelivery}))
|
|
assert.True(t, IsDeliveryOrReadReceipt(&ParsedWebhookEvent{EventType: EventFBRead}))
|
|
assert.True(t, IsDeliveryOrReadReceipt(&ParsedWebhookEvent{EventType: EventIGDelivery}))
|
|
assert.True(t, IsDeliveryOrReadReceipt(&ParsedWebhookEvent{EventType: EventIGRead}))
|
|
assert.False(t, IsDeliveryOrReadReceipt(&ParsedWebhookEvent{EventType: EventFBMessage}))
|
|
}
|
|
|
|
func TestShouldCreateMessage_NotEcho_Cov5(t *testing.T) {
|
|
assert.True(t, ShouldCreateMessage(&ParsedWebhookEvent{EventType: EventFBMessage}))
|
|
}
|
|
|
|
func TestShouldCreateMessage_Echo_Cov5(t *testing.T) {
|
|
assert.False(t, ShouldCreateMessage(&ParsedWebhookEvent{EventType: EventFBMessageEcho}))
|
|
}
|
|
|
|
func TestShouldCreateMessage_Receipt_Cov5(t *testing.T) {
|
|
assert.False(t, ShouldCreateMessage(&ParsedWebhookEvent{EventType: EventFBDelivery}))
|
|
}
|
|
|
|
func TestIsThreadControlEvent_All_Cov5(t *testing.T) {
|
|
assert.True(t, IsThreadControlEvent(&ParsedWebhookEvent{EventType: EventFBPassThread}))
|
|
assert.True(t, IsThreadControlEvent(&ParsedWebhookEvent{EventType: EventFBTakeThread}))
|
|
assert.True(t, IsThreadControlEvent(&ParsedWebhookEvent{EventType: EventFBRequestThread}))
|
|
assert.False(t, IsThreadControlEvent(&ParsedWebhookEvent{EventType: EventFBMessage}))
|
|
}
|
|
|
|
func TestInstagramOrFB_Instagram_Cov5(t *testing.T) {
|
|
assert.Equal(t, EventIGMessage, instagramOrFB(true, EventIGMessage, EventFBMessage))
|
|
}
|
|
|
|
func TestInstagramOrFB_Facebook_Cov5(t *testing.T) {
|
|
assert.Equal(t, EventFBMessage, instagramOrFB(false, EventIGMessage, EventFBMessage))
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_FBMessage_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBMessage,
|
|
Object: "page",
|
|
PageID: "page1",
|
|
SenderID: "s1",
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Text: "hello",
|
|
},
|
|
Timestamp: 1700000000000,
|
|
}
|
|
msg, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello", msg.Content)
|
|
assert.Equal(t, "m1", msg.SourceID)
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_Echo_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBMessageEcho,
|
|
}
|
|
_, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}}, channel.ChannelFacebook)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "does not create")
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_Postback_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBPostback,
|
|
PageID: "page1",
|
|
SenderID: "s1",
|
|
Postback: &FBPostback{
|
|
Title: "Get Started",
|
|
Data: "START",
|
|
},
|
|
Timestamp: 1700000000000,
|
|
}
|
|
msg, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "START", msg.Content)
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_PostbackWithReferral_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBPostback,
|
|
PageID: "page1",
|
|
SenderID: "s1",
|
|
Postback: &FBPostback{
|
|
Title: "Get Started",
|
|
Data: "START",
|
|
Referral: &FBReferral{Ref: "ref1", Source: "SHORTLINK"},
|
|
},
|
|
Timestamp: 1700000000000,
|
|
}
|
|
msg, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "START", msg.Content)
|
|
assert.Equal(t, "ref1", msg.Extra["referral_ref"])
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_WithAttachments_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBMessage,
|
|
PageID: "page1",
|
|
SenderID: "s1",
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Attachments: []FBAttachment{
|
|
{Type: "image", Payload: FBAttachmentPayload{URL: "http://img", Name: "img.png"}},
|
|
},
|
|
},
|
|
Timestamp: 1700000000000,
|
|
}
|
|
msg, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.Len(t, msg.Attachments, 1)
|
|
assert.Equal(t, "img.png", msg.Attachments[0].Filename)
|
|
}
|
|
|
|
func TestExtractIncomingMessageFromEvent_WithReplyTo_Cov5(t *testing.T) {
|
|
event := &ParsedWebhookEvent{
|
|
EventType: EventFBMessage,
|
|
PageID: "page1",
|
|
SenderID: "s1",
|
|
Message: &FBMessageData{
|
|
Mid: "m1",
|
|
Text: "reply",
|
|
ReplyTo: &FBReplyTo{Mid: "orig_mid"},
|
|
},
|
|
Timestamp: 1700000000000,
|
|
}
|
|
msg, err := ExtractIncomingMessageFromEvent(event, &model.Inbox{Base: model.Base{ID: 1}, AccountID: 1}, channel.ChannelFacebook)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "orig_mid", msg.ReplyToID)
|
|
}
|
|
|
|
func TestDetermineContentType_Image_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "image"}},
|
|
}
|
|
assert.Equal(t, channel.ContentImage, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Video_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "video"}},
|
|
}
|
|
assert.Equal(t, channel.ContentVideo, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Audio_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "audio"}},
|
|
}
|
|
assert.Equal(t, channel.ContentAudio, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_File_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "file"}},
|
|
}
|
|
assert.Equal(t, channel.ContentFile, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Location_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "location"}},
|
|
}
|
|
assert.Equal(t, channel.ContentLocation, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Fallback_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{
|
|
Attachments: []FBAttachment{{Type: "fallback"}},
|
|
}
|
|
assert.Equal(t, channel.ContentFile, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Text_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{Text: "hello"}
|
|
assert.Equal(t, channel.ContentText, determineContentType(msgData))
|
|
}
|
|
|
|
func TestDetermineContentType_Empty_Cov5(t *testing.T) {
|
|
msgData := &FBMessageData{}
|
|
assert.Equal(t, channel.ContentText, determineContentType(msgData))
|
|
}
|
|
|
|
func TestMillisToTime_Zero_Cov5(t *testing.T) {
|
|
result := millisToTime(0)
|
|
assert.True(t, result.After(time.Now().Add(-time.Minute)))
|
|
}
|
|
|
|
func TestMillisToTime_NonZero_Cov5(t *testing.T) {
|
|
result := millisToTime(1700000000000)
|
|
assert.Equal(t, int64(1700000000000), result.UnixMilli())
|
|
}
|
|
|
|
func TestConvertAttachments_Empty_Cov5(t *testing.T) {
|
|
result := convertAttachments(nil, channel.ChannelFacebook)
|
|
assert.Empty(t, result)
|
|
}
|
|
|
|
func TestConvertAttachments_WithLocation_Cov5(t *testing.T) {
|
|
atts := []FBAttachment{
|
|
{
|
|
Type: "location",
|
|
Payload: FBAttachmentPayload{
|
|
URL: "http://loc",
|
|
Name: "loc.json",
|
|
Coordinates: &FBLocationCoordinates{Lat: 1.5, Long: 2.5},
|
|
},
|
|
},
|
|
}
|
|
result := convertAttachments(atts, channel.ChannelFacebook)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, 1.5, result[0].Extra["latitude"])
|
|
assert.Equal(t, 2.5, result[0].Extra["longitude"])
|
|
}
|
|
|
|
func TestConvertAttachments_Image_Cov5(t *testing.T) {
|
|
atts := []FBAttachment{
|
|
{Type: "image", Payload: FBAttachmentPayload{URL: "http://img", Name: "img.png"}},
|
|
}
|
|
result := convertAttachments(atts, channel.ChannelInstagram)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, "img.png", result[0].Filename)
|
|
assert.Equal(t, "image/jpeg", result[0].ContentType)
|
|
}
|
|
|
|
// hmacSha256Hex computes HMAC-SHA256 and returns "sha256=<hex>".
|
|
func hmacSha256Hex(secret string, body []byte) string {
|
|
mac := hmac.New(sha256.New, []byte(secret))
|
|
mac.Write(body)
|
|
return "sha256=" + hex.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func TestIsNumeric_All_Cov5(t *testing.T) {
|
|
assert.True(t, isNumeric("12345"))
|
|
assert.False(t, isNumeric("abc"))
|
|
assert.False(t, isNumeric(""))
|
|
assert.False(t, isNumeric("12.34"))
|
|
}
|
|
|
|
func TestParseIGCommentChangeValue_Success_Cov5(t *testing.T) {
|
|
val := map[string]interface{}{
|
|
"media_id": "m1",
|
|
"id": "c1",
|
|
"text": "comment",
|
|
"from": map[string]interface{}{"id": "u1", "username": "user1"},
|
|
}
|
|
result, err := parseIGCommentChangeValue(val)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "m1", result.MediaID)
|
|
assert.Equal(t, "c1", result.CommentID)
|
|
assert.Equal(t, "comment", result.Text)
|
|
assert.Equal(t, "u1", result.From.ID)
|
|
}
|
|
|
|
func TestGetEnvOrDefault_Set_Cov5(t *testing.T) {
|
|
os.Setenv("GOCHAT_COV5_TEST_ENV", "testval")
|
|
defer os.Unsetenv("GOCHAT_COV5_TEST_ENV")
|
|
assert.Equal(t, "testval", getEnvOrDefault("GOCHAT_COV5_TEST_ENV", "default"))
|
|
}
|
|
|
|
func TestGetEnvOrDefault_Default_Cov5(t *testing.T) {
|
|
assert.Equal(t, "default", getEnvOrDefault("GOCHAT_COV5_NONEXISTENT_ENV", "default"))
|
|
}
|
|
|
|
func TestNewWebhookParser_Cov5(t *testing.T) {
|
|
wp := NewWebhookParser()
|
|
assert.NotNil(t, wp)
|
|
}
|
|
|
|
func TestNewMediaService_Cov5(t *testing.T) {
|
|
ms := NewMediaService("https://graph", "/tmp/test")
|
|
assert.NotNil(t, ms)
|
|
assert.Equal(t, "https://graph", ms.graphAPIBase)
|
|
assert.Equal(t, "/tmp/test", ms.storagePath)
|
|
}
|
|
|
|
func TestBytesReader_Cov5(t *testing.T) {
|
|
// Simple test to ensure bytes import is used
|
|
b := bytes.NewReader([]byte("test"))
|
|
assert.NotNil(t, b)
|
|
}
|
|
|
|
// ===========================
|
|
// FacebookProvider — ExchangeToken / RefreshToken additional (Cov5)
|
|
// ===========================
|
|
|
|
func TestFBProvider_ExchangeToken_NoAppID_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
p.appID = ""
|
|
p.appSecret = ""
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "App ID")
|
|
}
|
|
|
|
func TestFBProvider_ExchangeToken_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"access_token": "fb_token",
|
|
"expires_in": 3600,
|
|
"token_type": "bearer",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
result, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "fb_token", result.AccessToken)
|
|
assert.Equal(t, "bearer", result.Extra["token_type"])
|
|
}
|
|
|
|
func TestFBProvider_ExchangeToken_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 400")
|
|
}
|
|
|
|
func TestFBProvider_ExchangeToken_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
_, err := p.ExchangeToken(context.Background(), "code", "https://cb.example.com")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFBProvider_RefreshToken_NoToken_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
_, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "no current")
|
|
}
|
|
|
|
func TestFBProvider_RefreshToken_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"access_token": "new_fb_token",
|
|
"expires_in": 5000,
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = "appid"
|
|
p.appSecret = "secret"
|
|
|
|
result, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{
|
|
"page_access_token": "old_token",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "new_fb_token", result.AccessToken)
|
|
}
|
|
|
|
func TestFBProvider_RefreshToken_HTTPError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{
|
|
"page_access_token": "old_token",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "HTTP 401")
|
|
}
|
|
|
|
func TestFBProvider_RefreshToken_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.RefreshToken(context.Background(), &model.Inbox{}, channel.ChannelConfig{
|
|
"page_access_token": "old_token",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// FacebookProvider — ValidateWebhookRequest (Cov5)
|
|
// ===========================
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_GET_Success_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"webhook_verify_token": "mytoken"})
|
|
inbox := &model.Inbox{Base: model.Base{ID: 1}, ChannelConfig: string(configJSON)}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "subscribe", "hub.verify_token": "mytoken"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_GET_BadMode_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
inbox := &model.Inbox{}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "bad"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "hub.mode")
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_GET_TokenMismatch_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
configJSON, _ := json.Marshal(map[string]interface{}{"webhook_verify_token": "correct"})
|
|
inbox := &model.Inbox{ChannelConfig: string(configJSON)}
|
|
req := &channel.WebhookRequest{
|
|
Method: "GET",
|
|
QueryParams: map[string]string{"hub.mode": "subscribe", "hub.verify_token": "wrong"},
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "mismatch")
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_POST_NoSignature_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
inbox := &model.Inbox{}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "missing")
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_POST_NoSecret_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
p.appSecret = ""
|
|
inbox := &model.Inbox{}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": "sha256=abc"},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_POST_BadSig_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
p.appSecret = "secret"
|
|
inbox := &model.Inbox{}
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": "sha256=bad"},
|
|
Body: []byte("{}"),
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "signature")
|
|
}
|
|
|
|
func TestFBProvider_ValidateWebhookRequest_POST_ValidSig_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
p.appSecret = "secret"
|
|
inbox := &model.Inbox{}
|
|
body := []byte(`{"object":"page"}`)
|
|
sig := hmacSha256Hex(p.appSecret, body)
|
|
req := &channel.WebhookRequest{
|
|
Method: "POST",
|
|
Headers: map[string]string{"X-Hub-Signature-256": sig},
|
|
Body: body,
|
|
}
|
|
err := p.ValidateWebhookRequest(context.Background(), inbox, req)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// ===========================
|
|
// FacebookProvider — OnCreate / ValidateConfig (Cov5)
|
|
// ===========================
|
|
|
|
func TestFBProvider_OnCreate_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"name": "My Page",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
config := channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
}
|
|
result, err := p.OnCreate(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "My Page", result["page_name"])
|
|
}
|
|
|
|
func TestFBProvider_OnCreate_WithIG_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"name": "My Page",
|
|
"instagram_business_account": map[string]interface{}{"id": "ig_biz_1"},
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
config := channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
}
|
|
result, err := p.OnCreate(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "ig_biz_1", result["instagram_business_account_id"])
|
|
}
|
|
|
|
func TestFBProvider_OnCreate_NoAppID_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{"id": "page1", "name": "Page"})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
p.appID = ""
|
|
|
|
config := channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
}
|
|
result, err := p.OnCreate(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, config)
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, result)
|
|
}
|
|
|
|
func TestFBProvider_ValidateConfig_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "12345",
|
|
"name": "Test Page",
|
|
"access_token": "tok",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"page_id": "12345",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ValidateConfig_IDMismatch_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "different_id",
|
|
"name": "Test Page",
|
|
"access_token": "tok",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"page_id": "12345",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "mismatch")
|
|
}
|
|
|
|
func TestFBProvider_ValidateConfig_APIError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"page_id": "12345",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid")
|
|
}
|
|
|
|
func TestFBProvider_ValidateConfig_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"page_id": "12345",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "API call")
|
|
}
|
|
|
|
func TestFBProvider_CreateChannel_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "page1",
|
|
"name": "Test Page",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
ch, err := p.CreateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
"webhook_verify_token": "verify",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, ch)
|
|
}
|
|
|
|
func TestFBProvider_CreateChannel_APIError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.CreateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestFBProvider_CreateChannel_IDMismatch_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "different",
|
|
"name": "Page",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewFacebookProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
_, err := p.CreateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"page_id": "page1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "mismatch")
|
|
}
|
|
|
|
func TestFBProvider_UpdateChannel_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
ch, err := p.UpdateChannel(context.Background(), 1, channel.ChannelConfig{
|
|
"page_access_token": "newtok",
|
|
"page_name": "newname",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, ch)
|
|
}
|
|
|
|
func TestFBProvider_DeleteChannel_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
err := p.DeleteChannel(context.Background(), 1)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestFBProvider_ProcessIncomingMessage_Cov5(t *testing.T) {
|
|
p := NewFacebookProvider()
|
|
payload := map[string]interface{}{
|
|
"object": "page",
|
|
"entry": []interface{}{
|
|
map[string]interface{}{
|
|
"id": "page1",
|
|
"messaging": []interface{}{
|
|
map[string]interface{}{
|
|
"sender": map[string]interface{}{"id": "s1"},
|
|
"recipient": map[string]interface{}{"id": "r1"},
|
|
"message": map[string]interface{}{"mid": "m1", "text": "hello"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
msg, err := p.ProcessIncomingMessage(context.Background(), &model.Inbox{Base: model.Base{ID: 1}}, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "hello", msg.Content)
|
|
}
|
|
|
|
// ===========================
|
|
// InstagramProvider — ValidateConfig (Cov5)
|
|
// ===========================
|
|
|
|
func TestInstagramProvider_ValidateConfig_Success_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
mustEncodeTestJSON(w, map[string]interface{}{
|
|
"id": "ig1",
|
|
"username": "testuser",
|
|
})
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateConfig_APIError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "invalid")
|
|
}
|
|
|
|
func TestInstagramProvider_ValidateConfig_NetworkError_Cov5(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
|
srv.Close()
|
|
|
|
p := NewInstagramProvider()
|
|
p.graphAPIBase = srv.URL
|
|
|
|
err := p.ValidateConfig(context.Background(), channel.ChannelConfig{
|
|
"instagram_account_id": "ig1",
|
|
"page_access_token": "tok",
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "API call")
|
|
}
|