iachat/spec/helpers/baileys_helper_spec.rb
Gabriel Jablonski e188262877
fix(baileys): race condition send_message/incoming message (#51)
* fix: race condition send_message/incoming message

* fix: handle update race condition as well
2025-05-29 10:43:42 -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 and clears 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).to have_received(:delete).once.with(lock_key)
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