iachat/spec/helpers/baileys_helper_spec.rb
Gabriel Jablonski 8cf6e8907f
release v4.12.0-fazer-ai.47 (#259)
* fix(whatsapp): add idempotent message sending to prevent duplicates on timeout retry

When sending media messages via Baileys, Net::ReadTimeout causes Sidekiq to
retry the job, potentially sending the same message multiple times. This adds
a chatwootMessageId parameter to the Baileys API request, enabling server-side
deduplication via Redis. Also increases HTTP timeout to 120s and channel lock
to 130s to reduce false timeouts.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review feedback

- Use error.class.name assertions for parallel/reloading safety
- Assert reconnect endpoint was not called on 409 (stronger assertion)

* fix: address review feedback (round 2)

- Only release channel lock in ensure if it was actually acquired (prevents
  clearing another worker's lock on timeout)
- Assert chatwootMessageId in reproduction spec body matcher

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:52:26 -03:00

101 lines
4.0 KiB
Ruby

require 'rails_helper'
RSpec.describe BaileysHelper do
describe '#baileys_extract_message_timestamp' do
let(:timestamp_low) { 1_748_003_165 }
let(:timestamp_hash) { { 'low' => timestamp_low, 'high' => 1, 'unsigned' => true } }
it 'extracts the timestamp from a string' do
expect(baileys_extract_message_timestamp(timestamp_low.to_s)).to eq(timestamp_low)
end
it 'extracts the timestamp from an int' do
expect(baileys_extract_message_timestamp(timestamp_low)).to eq(timestamp_low)
end
it 'extracts the timestamp from a hash' do
expect(baileys_extract_message_timestamp(timestamp_hash)).to eq(6_042_970_461)
end
end
describe '#with_baileys_channel_lock_on_outgoing_message' do
let(:channel_id) { 1 }
let(:lock_key) { format(BaileysHelper::CHANNEL_LOCK_ON_OUTGOING_MESSAGE_KEY, channel_id: channel_id) }
let(:timeout) { BaileysHelper::CHANNEL_LOCK_ON_OUTGOING_MESSAGE_TIMEOUT }
before do
allow(Redis::Alfred).to receive(:set).and_return(true)
allow(Redis::Alfred).to receive(:delete)
end
context 'when a block is given' do
it 'yields to the block' do
expect { |b| with_baileys_channel_lock_on_outgoing_message(channel_id, &b) }.to yield_control
end
it 'attempts to acquire the lock' do
with_baileys_channel_lock_on_outgoing_message(channel_id) { nil }
expect(Redis::Alfred).to have_received(:set).with(lock_key, 1, nx: true, ex: timeout)
end
it 'clears the lock after the block executes' do
with_baileys_channel_lock_on_outgoing_message(channel_id) { nil }
expect(Redis::Alfred).to have_received(:delete).with(lock_key)
end
it 'clears the lock even if the block raises an error' do
expect do
with_baileys_channel_lock_on_outgoing_message(channel_id) { raise 'test error' }
end.to raise_error('test error')
expect(Redis::Alfred).to have_received(:delete).with(lock_key)
end
context 'when lock is acquired immediately' do
it 'executes the block and clears the lock' do
expect { |b| with_baileys_channel_lock_on_outgoing_message(channel_id, &b) }.to yield_control
expect(Redis::Alfred).to have_received(:set).with(lock_key, 1, nx: true, ex: timeout)
expect(Redis::Alfred).to have_received(:delete).with(lock_key)
end
end
context 'when lock is not acquired immediately but within timeout' do
it 'retries acquiring the lock, executes the block, and clears the lock' do
allow(Redis::Alfred).to receive(:set).and_return(false, true)
allow(self).to receive(:sleep)
expect { |b| with_baileys_channel_lock_on_outgoing_message(channel_id, &b) }.to yield_control
expect(Redis::Alfred).to have_received(:set).with(lock_key, 1, nx: true, ex: timeout).twice
expect(self).to have_received(:sleep).with(0.1).once
expect(Redis::Alfred).to have_received(:delete).once.with(lock_key)
end
end
context 'when lock is not acquired within timeout' do
it 'still executes the block but does not clear the lock' do
freeze_time
allow(Redis::Alfred).to receive(:set).and_return(false)
allow(self).to receive(:sleep) { travel_to 1.second.from_now }
expect { |b| with_baileys_channel_lock_on_outgoing_message(channel_id, &b) }.to yield_control
expect(Redis::Alfred).to have_received(:set)
.with(lock_key, 1, nx: true, ex: timeout)
.exactly(BaileysHelper::CHANNEL_LOCK_ON_OUTGOING_MESSAGE_TIMEOUT.to_i)
expect(Redis::Alfred).not_to have_received(:delete)
end
end
end
context 'when no block is given' do
it 'raises an ArgumentError' do
expect do
with_baileys_channel_lock_on_outgoing_message(channel_id)
end.to raise_error(ArgumentError,
'A block is required for with_baileys_channel_lock_on_outgoing_message')
end
end
end
end