fix: Flaky Instagram webhook specs (#12170)

### Summary

Fixed flaky Instagram webhook specs that failed intermittently in cloud
environments due to shared let blocks creating conflicting inboxes. The
Instagram channel factory already creates an inbox automatically, but
tests were adding extra ones in shared contexts.
Moved channel/inbox creation to isolated test contexts to prevent race
conditions between Facebook Page and Instagram Direct tests.

### Testing

```
for i in {1..30}; do 
  echo "=== Run $i ==="
  RAILS_ENV=test bundle exec rspec spec/jobs/webhooks/instagram_events_job_spec.rb --fail-fast || break
done
```

Previously, intermittent failures could be reproduced locally. With
these changes, tests achieve ~100% pass rate.
This commit is contained in:
Sojan Jose
2025-08-12 16:42:18 +02:00
committed by GitHub
parent 5c560c7628
commit ad4ec9e93b

View File

@@ -10,23 +10,6 @@ describe Webhooks::InstagramEventsJob do
end end
let!(:account) { create(:account) } let!(:account) { create(:account) }
let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) }
let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) }
# Combined message events into one helper
let(:message_events) do
{
dm: build(:instagram_message_create_event).with_indifferent_access,
standby: build(:instagram_message_standby_event).with_indifferent_access,
unsend: build(:instagram_message_unsend_event).with_indifferent_access,
attachment: build(:instagram_message_attachment_event).with_indifferent_access,
story_mention: build(:instagram_story_mention_event).with_indifferent_access,
story_mention_echo: build(:instagram_story_mention_event_with_echo).with_indifferent_access,
messaging_seen: build(:messaging_seen_event).with_indifferent_access,
unsupported: build(:instagram_message_unsupported_event).with_indifferent_access
}
end
def return_object_for(sender_id) def return_object_for(sender_id)
{ name: 'Jane', { name: 'Jane',
@@ -38,21 +21,19 @@ describe Webhooks::InstagramEventsJob do
describe '#perform' do describe '#perform' do
context 'when handling messaging events for Instagram via Facebook page' do context 'when handling messaging events for Instagram via Facebook page' do
let!(:instagram_messenger_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_messenger_inbox) { create(:inbox, channel: instagram_messenger_channel, account: account, greeting_enabled: false) }
let(:fb_object) { double } let(:fb_object) { double }
before do
instagram_inbox.destroy
end
it 'creates incoming message in the instagram inbox' do it 'creates incoming message in the instagram inbox' do
dm_event = build(:instagram_message_create_event).with_indifferent_access
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access return_object_for(sender_id).with_indifferent_access
) )
instagram_webhook.perform_now(message_events[:dm][:entry]) instagram_webhook.perform_now(dm_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.contacts.count).to be 1 expect(instagram_messenger_inbox.contacts.count).to be 1
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name' expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
@@ -62,14 +43,14 @@ describe Webhooks::InstagramEventsJob do
end end
it 'creates standby message in the instagram inbox' do it 'creates standby message in the instagram inbox' do
standby_event = build(:instagram_message_standby_event).with_indifferent_access
sender_id = standby_event[:entry][0][:standby][0][:sender][:id]
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:standby][:entry][0][:standby][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access return_object_for(sender_id).with_indifferent_access
) )
instagram_webhook.perform_now(message_events[:standby][:entry]) instagram_webhook.perform_now(standby_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.contacts.count).to be 1 expect(instagram_messenger_inbox.contacts.count).to be 1
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name' expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
@@ -81,9 +62,11 @@ describe Webhooks::InstagramEventsJob do
end end
it 'handle instagram unsend message event' do it 'handle instagram unsend message event' do
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
sender_id = unsend_event[:entry][0][:messaging][0][:sender][:id]
message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete') message = create(:message, inbox_id: instagram_messenger_inbox.id, source_id: 'message-id-to-delete')
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:unsend][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
{ {
name: 'Jane', name: 'Jane',
@@ -96,7 +79,7 @@ describe Webhooks::InstagramEventsJob do
expect(instagram_messenger_inbox.messages.count).to be 1 expect(instagram_messenger_inbox.messages.count).to be 1
instagram_webhook.perform_now(message_events[:unsend][:entry]) instagram_webhook.perform_now(unsend_event[:entry])
expect(instagram_messenger_inbox.messages.last.content).to eq 'This message was deleted' expect(instagram_messenger_inbox.messages.last.content).to eq 'This message was deleted'
expect(instagram_messenger_inbox.messages.last.deleted).to be true expect(instagram_messenger_inbox.messages.last.deleted).to be true
@@ -105,14 +88,14 @@ describe Webhooks::InstagramEventsJob do
end end
it 'creates incoming message with attachments in the instagram inbox' do it 'creates incoming message with attachments in the instagram inbox' do
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
sender_id = attachment_event[:entry][0][:messaging][0][:sender][:id]
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:attachment][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access return_object_for(sender_id).with_indifferent_access
) )
instagram_webhook.perform_now(message_events[:attachment][:entry]) instagram_webhook.perform_now(attachment_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.contacts.count).to be 1 expect(instagram_messenger_inbox.contacts.count).to be 1
expect(instagram_messenger_inbox.messages.count).to be 1 expect(instagram_messenger_inbox.messages.count).to be 1
@@ -120,8 +103,10 @@ describe Webhooks::InstagramEventsJob do
end end
it 'creates incoming message with attachments in the instagram inbox for story mention' do it 'creates incoming message with attachments in the instagram inbox for story mention' do
story_mention_event = build(:instagram_story_mention_event).with_indifferent_access
sender_id = story_mention_event[:entry][0][:messaging][0][:sender][:id]
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:story_mention][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access, return_object_for(sender_id).with_indifferent_access,
{ story: { story:
@@ -137,9 +122,7 @@ describe Webhooks::InstagramEventsJob do
id: 'instagram-message-id-1234' }.with_indifferent_access id: 'instagram-message-id-1234' }.with_indifferent_access
) )
instagram_webhook.perform_now(message_events[:story_mention][:entry]) instagram_webhook.perform_now(story_mention_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.messages.count).to be 1 expect(instagram_messenger_inbox.messages.count).to be 1
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1 expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
@@ -149,12 +132,12 @@ describe Webhooks::InstagramEventsJob do
end end
it 'does not create contact or messages when Facebook API call fails' do it 'does not create contact or messages when Facebook API call fails' do
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError) allow(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError)
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry]) instagram_webhook.perform_now(story_mention_echo_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.contacts.count).to be 0 expect(instagram_messenger_inbox.contacts.count).to be 0
expect(instagram_messenger_inbox.contact_inboxes.count).to be 0 expect(instagram_messenger_inbox.contact_inboxes.count).to be 0
@@ -162,21 +145,23 @@ describe Webhooks::InstagramEventsJob do
end end
it 'handle messaging_seen callback' do it 'handle messaging_seen callback' do
expect(Instagram::ReadStatusService).to receive(:new).with(params: message_events[:messaging_seen][:entry][0][:messaging][0], messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
channel: instagram_messenger_inbox.channel).and_call_original channel: instagram_messenger_inbox.channel).and_call_original
instagram_webhook.perform_now(message_events[:messaging_seen][:entry]) instagram_webhook.perform_now(messaging_seen_event[:entry])
end end
it 'handles unsupported message' do it 'handles unsupported message' do
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
sender_id = unsupported_event[:entry][0][:messaging][0][:sender][:id]
allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(Koala::Facebook::API).to receive(:new).and_return(fb_object)
sender_id = message_events[:unsupported][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return( allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access return_object_for(sender_id).with_indifferent_access
) )
instagram_webhook.perform_now(message_events[:unsupported][:entry]) instagram_webhook.perform_now(unsupported_event[:entry])
instagram_messenger_inbox.reload
expect(instagram_messenger_inbox.contacts.count).to be 1 expect(instagram_messenger_inbox.contacts.count).to be 1
expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name' expect(instagram_messenger_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
expect(instagram_messenger_inbox.conversations.count).to be 1 expect(instagram_messenger_inbox.conversations.count).to be 1
@@ -186,6 +171,9 @@ describe Webhooks::InstagramEventsJob do
end end
context 'when handling messaging events for Instagram via Instagram login' do context 'when handling messaging events for Instagram via Instagram login' do
let!(:instagram_channel) { create(:channel_instagram, account: account, instagram_id: 'chatwoot-app-user-id-1') }
let!(:instagram_inbox) { instagram_channel.inbox }
before do before do
instagram_channel.update(access_token: 'valid_instagram_token') instagram_channel.update(access_token: 'valid_instagram_token')
@@ -210,9 +198,8 @@ describe Webhooks::InstagramEventsJob do
end end
it 'creates incoming message with correct contact info in the instagram direct inbox' do it 'creates incoming message with correct contact info in the instagram direct inbox' do
instagram_webhook.perform_now(message_events[:dm][:entry]) dm_event = build(:instagram_message_create_event).with_indifferent_access
instagram_inbox.reload instagram_webhook.perform_now(dm_event[:entry])
expect(instagram_inbox.contacts.count).to eq 1 expect(instagram_inbox.contacts.count).to eq 1
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name' expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
expect(instagram_inbox.conversations.count).to eq 1 expect(instagram_inbox.conversations.count).to eq 1
@@ -221,7 +208,8 @@ describe Webhooks::InstagramEventsJob do
end end
it 'sets correct instagram attributes on contact' do it 'sets correct instagram attributes on contact' do
instagram_webhook.perform_now(message_events[:dm][:entry]) dm_event = build(:instagram_message_create_event).with_indifferent_access
instagram_webhook.perform_now(dm_event[:entry])
instagram_inbox.reload instagram_inbox.reload
contact = instagram_inbox.contacts.last contact = instagram_inbox.contacts.last
@@ -233,6 +221,8 @@ describe Webhooks::InstagramEventsJob do
end end
it 'handle instagram unsend message event' do it 'handle instagram unsend message event' do
unsend_event = build(:instagram_message_unsend_event).with_indifferent_access
message = create(:message, inbox_id: instagram_inbox.id, source_id: 'message-id-to-delete', content: 'random_text') message = create(:message, inbox_id: instagram_inbox.id, source_id: 'message-id-to-delete', content: 'random_text')
# Create attachment correctly with account association # Create attachment correctly with account association
@@ -244,7 +234,7 @@ describe Webhooks::InstagramEventsJob do
expect(instagram_inbox.messages.count).to be 1 expect(instagram_inbox.messages.count).to be 1
instagram_webhook.perform_now(message_events[:unsend][:entry]) instagram_webhook.perform_now(unsend_event[:entry])
message.reload message.reload
@@ -254,9 +244,8 @@ describe Webhooks::InstagramEventsJob do
end end
it 'creates incoming message with attachments in the instagram direct inbox' do it 'creates incoming message with attachments in the instagram direct inbox' do
instagram_webhook.perform_now(message_events[:attachment][:entry]) attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
instagram_webhook.perform_now(attachment_event[:entry])
instagram_inbox.reload
expect(instagram_inbox.contacts.count).to be 1 expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.messages.count).to be 1 expect(instagram_inbox.messages.count).to be 1
@@ -264,9 +253,8 @@ describe Webhooks::InstagramEventsJob do
end end
it 'handles unsupported message' do it 'handles unsupported message' do
instagram_webhook.perform_now(message_events[:unsupported][:entry]) unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
instagram_inbox.reload instagram_webhook.perform_now(unsupported_event[:entry])
expect(instagram_inbox.contacts.count).to be 1 expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name' expect(instagram_inbox.contacts.last.additional_attributes['social_instagram_user_name']).to eq 'some_user_name'
expect(instagram_inbox.conversations.count).to be 1 expect(instagram_inbox.conversations.count).to be 1
@@ -275,12 +263,12 @@ describe Webhooks::InstagramEventsJob do
end end
it 'does not create contact or messages when Instagram API call fails' do it 'does not create contact or messages when Instagram API call fails' do
story_mention_echo_event = build(:instagram_story_mention_event_with_echo).with_indifferent_access
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*}) stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json) .to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry]) instagram_webhook.perform_now(story_mention_echo_event[:entry])
instagram_inbox.reload
expect(instagram_inbox.contacts.count).to be 0 expect(instagram_inbox.contacts.count).to be 0
expect(instagram_inbox.contact_inboxes.count).to be 0 expect(instagram_inbox.contact_inboxes.count).to be 0
@@ -288,19 +276,20 @@ describe Webhooks::InstagramEventsJob do
end end
it 'handles messaging_seen callback' do it 'handles messaging_seen callback' do
expect(Instagram::ReadStatusService).to receive(:new).with(params: message_events[:messaging_seen][:entry][0][:messaging][0], messaging_seen_event = build(:messaging_seen_event).with_indifferent_access
expect(Instagram::ReadStatusService).to receive(:new).with(params: messaging_seen_event[:entry][0][:messaging][0],
channel: instagram_inbox.channel).and_call_original channel: instagram_inbox.channel).and_call_original
instagram_webhook.perform_now(message_events[:messaging_seen][:entry]) instagram_webhook.perform_now(messaging_seen_event[:entry])
end end
it 'creates contact when Instagram API call returns `No matching Instagram user` (9010 error code)' do it 'creates contact when Instagram API call returns `No matching Instagram user` (9010 error code)' do
stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*}) stub_request(:get, %r{https://graph\.instagram\.com/v22\.0/.*\?.*})
.to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json) .to_return(status: 401, body: { error: { message: 'No matching Instagram user', code: 9010 } }.to_json)
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id] dm_event = build(:instagram_message_create_event).with_indifferent_access
instagram_webhook.perform_now(message_events[:dm][:entry]) sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
instagram_webhook.perform_now(dm_event[:entry])
instagram_inbox.reload
expect(instagram_inbox.contacts.count).to be 1 expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})" expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})"