Message pagination now constrains client-provided cursors to the PostgreSQL integer range used by `messages.id`, preventing oversized values from raising database errors while preserving before/after pagination semantics. ## Closes - [CW-7922](https://linear.app/chatwoot/issue/CW-7922/harden-backend-paths-causing-production-sentry-errors) - [Sentry 7663397140](https://chatwoot-p3.sentry.io/issues/7663397140/) - [Sentry 7663397546](https://chatwoot-p3.sentry.io/issues/7663397546/) - [Sentry 7663464772](https://chatwoot-p3.sentry.io/issues/7663464772/) ## How to reproduce Request conversation messages with an extremely large `before` or `after` cursor, such as `4611686018427387903`. PostgreSQL previously rejected the value as outside the range for the integer `messages.id` column. ## What changed - Normalize message cursors before they reach the query. - Clamp them to the valid signed 32-bit integer ID range. - Cover oversized `before` and `after` cursors with finder specs.
116 lines
3.8 KiB
Ruby
116 lines
3.8 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe MessageFinder do
|
|
subject(:message_finder) { described_class.new(conversation, params) }
|
|
|
|
let!(:account) { create(:account) }
|
|
let!(:user) { create(:user, account: account) }
|
|
let!(:inbox) { create(:inbox, account: account) }
|
|
let!(:contact) { create(:contact, email: nil) }
|
|
let!(:conversation) do
|
|
create(:conversation, account: account, inbox: inbox, assignee: user, contact: contact)
|
|
end
|
|
|
|
before do
|
|
create(:message, account: account, inbox: inbox, conversation: conversation)
|
|
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
|
|
create(:message, message_type: 'activity', account: account, inbox: inbox, conversation: conversation)
|
|
# this outgoing message creates 2 additional messages because of the email hook execution service
|
|
create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation)
|
|
end
|
|
|
|
describe '#perform' do
|
|
context 'with filter_internal_messages false' do
|
|
let(:params) { { filter_internal_messages: false } }
|
|
|
|
it 'filter conversations by status' do
|
|
result = message_finder.perform
|
|
expect(result.count).to be 6
|
|
end
|
|
end
|
|
|
|
context 'with filter_internal_messages true' do
|
|
let(:params) { { filter_internal_messages: true } }
|
|
|
|
it 'filter conversations by status' do
|
|
result = message_finder.perform
|
|
expect(result.count).to be 4
|
|
end
|
|
end
|
|
|
|
context 'with before attribute' do
|
|
let!(:outgoing) { create(:message, message_type: 'outgoing', account: account, inbox: inbox, conversation: conversation) }
|
|
let(:params) { { before: outgoing.id } }
|
|
|
|
it 'filter conversations by status' do
|
|
result = message_finder.perform
|
|
expect(result.count).to be 6
|
|
end
|
|
end
|
|
|
|
context 'with a before attribute above the message id range' do
|
|
let!(:max_id_message) do
|
|
create(:message, id: described_class::MESSAGE_ID_MAX, account: account, inbox: inbox, conversation: conversation)
|
|
end
|
|
let(:params) { { before: 4_611_686_018_427_387_903 } }
|
|
|
|
it 'includes the maximum valid message id without overflowing the database column' do
|
|
expect(message_finder.perform).to include(max_id_message)
|
|
end
|
|
end
|
|
|
|
context 'with after attribute' do
|
|
let(:params) { { after: conversation.messages.first.id } }
|
|
|
|
it 'filter conversations by status' do
|
|
result = message_finder.perform
|
|
expect(result.count).to be 5
|
|
expect(result.first.id).to be conversation.messages.second.id
|
|
expect(result.last.message_type).to eq 'outgoing'
|
|
end
|
|
end
|
|
|
|
context 'with an after attribute above the message id range' do
|
|
let(:params) { { after: 881_965_304_328 } }
|
|
|
|
it 'returns no messages without overflowing the database column' do
|
|
expect(message_finder.perform).to be_empty
|
|
end
|
|
end
|
|
|
|
context 'with after and before attribute' do
|
|
let(:params) do
|
|
{
|
|
after: conversation.messages.first.id,
|
|
before: conversation.messages.last.id
|
|
}
|
|
end
|
|
|
|
it 'filter conversations by status' do
|
|
result = message_finder.perform
|
|
expect(result.count).to be 5
|
|
expect(result.last.id).to be conversation.messages[-2].id
|
|
end
|
|
end
|
|
|
|
context 'with after and before attributes above the message id range' do
|
|
let!(:max_id_message) do
|
|
create(:message, id: described_class::MESSAGE_ID_MAX, account: account, inbox: inbox, conversation: conversation)
|
|
end
|
|
let(:params) do
|
|
{
|
|
after: 881_965_304_328,
|
|
before: 4_611_686_018_427_387_903
|
|
}
|
|
end
|
|
|
|
it 'returns no messages' do
|
|
result = message_finder.perform
|
|
|
|
expect(result).to be_empty
|
|
expect(result).not_to include(max_id_message)
|
|
end
|
|
end
|
|
end
|
|
end
|