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