Files
gochat/backend/internal/channel/facebook/coverage5_test.go
T
Rogeeandrogee 2b182f9956 H-300: wire Captain Skills into Web runtime (#48)
* 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>
2026-08-19 07:08:14 +08:00

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")
}