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
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)
{ name: 'Jane',
@@ -38,21 +21,19 @@ describe Webhooks::InstagramEventsJob do
describe '#perform' 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 }
before do
instagram_inbox.destroy
end
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)
sender_id = message_events[:dm][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:dm][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(dm_event[:entry])
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'
@@ -62,14 +43,14 @@ describe Webhooks::InstagramEventsJob do
end
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)
sender_id = message_events[:standby][:entry][0][:standby][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:standby][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(standby_event[:entry])
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'
@@ -81,9 +62,11 @@ describe Webhooks::InstagramEventsJob do
end
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')
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(
{
name: 'Jane',
@@ -96,7 +79,7 @@ describe Webhooks::InstagramEventsJob do
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.deleted).to be true
@@ -105,14 +88,14 @@ describe Webhooks::InstagramEventsJob do
end
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)
sender_id = message_events[:attachment][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:attachment][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(attachment_event[:entry])
expect(instagram_messenger_inbox.contacts.count).to be 1
expect(instagram_messenger_inbox.messages.count).to be 1
@@ -120,8 +103,10 @@ describe Webhooks::InstagramEventsJob do
end
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)
sender_id = message_events[:story_mention][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access,
{ story:
@@ -137,9 +122,7 @@ describe Webhooks::InstagramEventsJob do
id: 'instagram-message-id-1234' }.with_indifferent_access
)
instagram_webhook.perform_now(message_events[:story_mention][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(story_mention_event[:entry])
expect(instagram_messenger_inbox.messages.count).to be 1
expect(instagram_messenger_inbox.messages.last.attachments.count).to be 1
@@ -149,12 +132,12 @@ describe Webhooks::InstagramEventsJob do
end
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(fb_object).to receive(:get_object).and_raise(Koala::Facebook::ClientError)
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(story_mention_echo_event[:entry])
expect(instagram_messenger_inbox.contacts.count).to be 0
expect(instagram_messenger_inbox.contact_inboxes.count).to be 0
@@ -162,21 +145,23 @@ describe Webhooks::InstagramEventsJob do
end
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
instagram_webhook.perform_now(message_events[:messaging_seen][:entry])
instagram_webhook.perform_now(messaging_seen_event[:entry])
end
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)
sender_id = message_events[:unsupported][:entry][0][:messaging][0][:sender][:id]
allow(fb_object).to receive(:get_object).and_return(
return_object_for(sender_id).with_indifferent_access
)
instagram_webhook.perform_now(message_events[:unsupported][:entry])
instagram_messenger_inbox.reload
instagram_webhook.perform_now(unsupported_event[:entry])
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.conversations.count).to be 1
@@ -186,6 +171,9 @@ describe Webhooks::InstagramEventsJob do
end
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
instagram_channel.update(access_token: 'valid_instagram_token')
@@ -210,9 +198,8 @@ describe Webhooks::InstagramEventsJob do
end
it 'creates incoming message with correct contact info in the instagram direct inbox' do
instagram_webhook.perform_now(message_events[:dm][:entry])
instagram_inbox.reload
dm_event = build(:instagram_message_create_event).with_indifferent_access
instagram_webhook.perform_now(dm_event[:entry])
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.conversations.count).to eq 1
@@ -221,7 +208,8 @@ describe Webhooks::InstagramEventsJob do
end
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
contact = instagram_inbox.contacts.last
@@ -233,6 +221,8 @@ describe Webhooks::InstagramEventsJob do
end
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')
# Create attachment correctly with account association
@@ -244,7 +234,7 @@ describe Webhooks::InstagramEventsJob do
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
@@ -254,9 +244,8 @@ describe Webhooks::InstagramEventsJob do
end
it 'creates incoming message with attachments in the instagram direct inbox' do
instagram_webhook.perform_now(message_events[:attachment][:entry])
instagram_inbox.reload
attachment_event = build(:instagram_message_attachment_event).with_indifferent_access
instagram_webhook.perform_now(attachment_event[:entry])
expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.messages.count).to be 1
@@ -264,9 +253,8 @@ describe Webhooks::InstagramEventsJob do
end
it 'handles unsupported message' do
instagram_webhook.perform_now(message_events[:unsupported][:entry])
instagram_inbox.reload
unsupported_event = build(:instagram_message_unsupported_event).with_indifferent_access
instagram_webhook.perform_now(unsupported_event[:entry])
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.conversations.count).to be 1
@@ -275,12 +263,12 @@ describe Webhooks::InstagramEventsJob do
end
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/.*\?.*})
.to_return(status: 401, body: { error: { message: 'Invalid OAuth access token' } }.to_json)
instagram_webhook.perform_now(message_events[:story_mention_echo][:entry])
instagram_inbox.reload
instagram_webhook.perform_now(story_mention_echo_event[:entry])
expect(instagram_inbox.contacts.count).to be 0
expect(instagram_inbox.contact_inboxes.count).to be 0
@@ -288,19 +276,20 @@ describe Webhooks::InstagramEventsJob do
end
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
instagram_webhook.perform_now(message_events[:messaging_seen][:entry])
instagram_webhook.perform_now(messaging_seen_event[:entry])
end
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/.*\?.*})
.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]
instagram_webhook.perform_now(message_events[:dm][:entry])
instagram_inbox.reload
dm_event = build(:instagram_message_create_event).with_indifferent_access
sender_id = dm_event[:entry][0][:messaging][0][:sender][:id]
instagram_webhook.perform_now(dm_event[:entry])
expect(instagram_inbox.contacts.count).to be 1
expect(instagram_inbox.contacts.last.name).to eq "Unknown (IG: #{sender_id})"