chore: upgrade Rails to 7.2.3.1 (#13437)
This upgrades Chatwoot to Rails 7.2.3.1 while retaining the current Rails 7.0 framework defaults, so the runtime upgrade can be deployed and observed independently from default-behavior changes. ## What changed - Upgrade Rails and the compatible dependency set to Rails 7.2.3.1. - Keep `config.load_defaults 7.0` for a staged, lower-risk rollout. - Replace the unmaintained Azure Active Storage fork with the maintained `azure-blob` adapter while preserving the `microsoft` service name. - Pin Sidekiq 7.3.10 with `connection_pool` 2.x after validating scheduled-job execution against Redis. - Update Rails 7.2 compatibility surfaces in Active Record, strong parameters, migrations, storage, and tests. - Add read-only production preflight checks, an opt-in Active Storage smoke script, a deployment runbook, and the full Rails 7.2/8.0/8.1 assessment. ## How to test 1. Sign in and verify the dashboard and conversation UI load normally. 2. Open the agent-management modal and confirm agent data is rendered. 3. Create an API inbox and wait for the asynchronous deletion flow to complete. 4. Open Super Admin pages, including instance status and account-user management. 5. Upload and download an attachment using the configured Active Storage service. 6. Confirm recurring Sidekiq Cron jobs register and execute after startup. ## Rollout Follow `docs/rails_upgrades/7_2.md` for pre-deploy checks, deployment order, smoke tests, monitoring, and rollback. Run `bundle exec rails runner script/rails_upgrade/preflight.rb` against a production-equivalent environment before rollout. ## Tracking - [CW-5863 — Upgrade Rails to 8+](https://linear.app/chatwoot/issue/CW-5863/upgrade-rails-to-8) - [Rails 7.2 to 8.1 upgrade and production rollout plan](https://linear.app/chatwoot/document/chatwoot-rails-72-to-81-upgrade-and-production-rollout-plan-44e9f4964cb2) --------- Co-authored-by: Shivam Mishra <scm.mymail@gmail.com> Co-authored-by: Sony Mathew <2040199+sony-mathew@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -407,7 +407,7 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
|
||||
headers: administrator.create_new_auth_token
|
||||
end
|
||||
|
||||
expect(conversation.messages.activity.last.content).to eq("Assigned to #{user_1.name} by #{administrator.name}")
|
||||
expect(conversation.reload.assignee_id).to eq(user_1.id)
|
||||
end
|
||||
|
||||
it 'Assign the agent when he is not inbox member' do
|
||||
@@ -421,7 +421,7 @@ RSpec.describe 'Api::V1::Accounts::MacrosController', type: :request do
|
||||
headers: administrator.create_new_auth_token
|
||||
end
|
||||
|
||||
expect(conversation.messages.activity.last.content).not_to eq("Assigned to #{user_1.name} by #{administrator.name}")
|
||||
expect(conversation.reload.assignee_id).to be_nil
|
||||
end
|
||||
|
||||
it 'Assign the labels' do
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_context 'with smtp config' do
|
||||
before do
|
||||
# We need to use allow_any_instance_of here because smtp_config_set_or_development?
|
||||
# is defined in ApplicationMailer and needs to be stubbed for all mailer instances
|
||||
# rubocop:disable RSpec/AnyInstance
|
||||
allow_any_instance_of(ApplicationMailer).to receive(:smtp_config_set_or_development?).and_return(true)
|
||||
# rubocop:enable RSpec/AnyInstance
|
||||
around do |example|
|
||||
# Set SMTP_ADDRESS so mailers build a Mail::Message in test without touching real SMTP.
|
||||
# Scoped to this shared context to avoid affecting other specs.
|
||||
with_modified_env('SMTP_ADDRESS' => 'smtp.example.com') { example.run }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -28,8 +28,7 @@ RSpec.describe ContactInbox do
|
||||
obj.reload
|
||||
|
||||
# ensure the column is nil in database
|
||||
results = ActiveRecord::Base.connection.execute('Select * from contact_inboxes;')
|
||||
expect(results.first['pubsub_token']).to be_nil
|
||||
expect(described_class.where(id: obj.id).pick(:pubsub_token)).to be_nil
|
||||
|
||||
new_token = obj.pubsub_token
|
||||
obj.update(source_id: '234234323')
|
||||
|
||||
@@ -45,7 +45,7 @@ end
|
||||
RSpec.configure do |config|
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
|
||||
config.fixture_path = Rails.root.join('spec/fixtures')
|
||||
config.fixture_paths = [Rails.root.join('spec/fixtures')]
|
||||
|
||||
# If you're not using ActiveRecord, or you'd prefer not to run each of your
|
||||
# examples within a transaction, remove the following line or assign false
|
||||
|
||||
@@ -42,6 +42,11 @@ describe Telegram::IncomingMessageService do
|
||||
}
|
||||
end
|
||||
|
||||
def contact_for(source_id = nil)
|
||||
source_id ||= message_params.dig('from', 'id')
|
||||
ContactInbox.find_by!(inbox: telegram_channel.inbox, source_id: source_id).contact
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
context 'when valid text message params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
@@ -51,7 +56,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.content).to eq('test')
|
||||
end
|
||||
end
|
||||
@@ -64,9 +69,9 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(23)
|
||||
expect(Contact.all.first.additional_attributes['social_telegram_user_name']).to eq('sojan')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(contact_for.additional_attributes['social_telegram_user_id']).to eq(23)
|
||||
expect(contact_for.additional_attributes['social_telegram_user_name']).to eq('sojan')
|
||||
expect(telegram_channel.inbox.messages.first.content).to eq('test')
|
||||
end
|
||||
end
|
||||
@@ -107,7 +112,7 @@ describe Telegram::IncomingMessageService do
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
|
||||
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
|
||||
contact = Contact.all.first
|
||||
contact = contact_for
|
||||
expect(contact.name).to eq('Sojan Jose')
|
||||
expect(contact.additional_attributes['language_code']).to eq('en')
|
||||
message = telegram_channel.inbox.messages.first
|
||||
@@ -131,7 +136,7 @@ describe Telegram::IncomingMessageService do
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
|
||||
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
|
||||
contact = Contact.all.first
|
||||
contact = contact_for
|
||||
expect(contact.name).to eq('Sojan Jose')
|
||||
# TODO: The language code is not present when we send the first message to the client.
|
||||
# Should we update it when the user replies?
|
||||
@@ -161,9 +166,9 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(23)
|
||||
expect(Contact.all.first.additional_attributes['social_telegram_user_name']).to eq('sojan')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(contact_for.additional_attributes['social_telegram_user_id']).to eq(23)
|
||||
expect(contact_for.additional_attributes['social_telegram_user_name']).to eq('sojan')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('audio')
|
||||
end
|
||||
end
|
||||
@@ -182,7 +187,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
@@ -207,7 +212,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('image')
|
||||
end
|
||||
end
|
||||
@@ -229,7 +234,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
|
||||
end
|
||||
end
|
||||
@@ -258,7 +263,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('video')
|
||||
end
|
||||
end
|
||||
@@ -277,7 +282,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('audio')
|
||||
end
|
||||
end
|
||||
@@ -298,7 +303,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
attachment = telegram_channel.inbox.messages.first.attachments.first
|
||||
expect(attachment.file_type).to eq('file')
|
||||
# Retain the original filename from the payload instead of Telegram's internal download name
|
||||
@@ -358,7 +363,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('location')
|
||||
end
|
||||
|
||||
@@ -377,7 +382,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
|
||||
attachment = telegram_channel.inbox.messages.first.attachments.first
|
||||
expect(attachment.file_type).to eq('location')
|
||||
@@ -410,8 +415,8 @@ describe Telegram::IncomingMessageService do
|
||||
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(Contact.all.first.additional_attributes['social_telegram_user_id']).to eq(5_171_248)
|
||||
expect(contact_for(5_171_248).name).to eq('Sojan Jose')
|
||||
expect(contact_for(5_171_248).additional_attributes['social_telegram_user_id']).to eq(5_171_248)
|
||||
expect(telegram_channel.inbox.messages.first.content).to eq('Option 1')
|
||||
end
|
||||
end
|
||||
@@ -428,7 +433,7 @@ describe Telegram::IncomingMessageService do
|
||||
}.with_indifferent_access
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_for.name).to eq('Sojan Jose')
|
||||
expect(telegram_channel.inbox.messages.first.attachments.first.file_type).to eq('contact')
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
end
|
||||
|
||||
let!(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', sync_templates: false, validate_provider_config: false) }
|
||||
let(:sender_number) { '2423423243' }
|
||||
let(:params) do
|
||||
{
|
||||
phone_number: whatsapp_channel.phone_number,
|
||||
@@ -14,9 +15,9 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }],
|
||||
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: sender_number }],
|
||||
messages: [{
|
||||
from: '2423423243',
|
||||
from: sender_number,
|
||||
image: {
|
||||
id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683',
|
||||
mime_type: 'image/jpeg',
|
||||
@@ -52,7 +53,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect_contact_name
|
||||
expect(whatsapp_channel.inbox.messages.first.content).to eq('Check out my product!')
|
||||
expect(whatsapp_channel.inbox.messages.first.attachments.present?).to be false
|
||||
expect(whatsapp_channel.authorization_error_count).to eq(1)
|
||||
@@ -197,9 +198,9 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
entry: [{
|
||||
changes: [{
|
||||
value: {
|
||||
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }],
|
||||
contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: sender_number }],
|
||||
messages: [{
|
||||
from: '2423423243',
|
||||
from: sender_number,
|
||||
image: {
|
||||
id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683',
|
||||
mime_type: 'image/jpeg',
|
||||
@@ -222,7 +223,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
it 'with attachment errors' do
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: error_params).perform
|
||||
expect(whatsapp_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect_contact_name
|
||||
expect(whatsapp_channel.inbox.messages.count).to eq(0)
|
||||
end
|
||||
end
|
||||
@@ -324,7 +325,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
described_class.new(inbox: whatsapp_channel.inbox, params: { phone_number: whatsapp_channel.phone_number,
|
||||
object: 'whatsapp_business_account', entry: {} }).perform
|
||||
expect(whatsapp_channel.inbox.conversations.count).to eq(0)
|
||||
expect(Contact.all.first).to be_nil
|
||||
expect(Contact.find_by(phone_number: contact_phone_number)).to be_nil
|
||||
expect(whatsapp_channel.inbox.messages.count).to eq(0)
|
||||
end
|
||||
end
|
||||
@@ -503,7 +504,7 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
end
|
||||
|
||||
def expect_contact_name
|
||||
expect(Contact.all.first.name).to eq('Sojan Jose')
|
||||
expect(contact_from_number&.name).to eq('Sojan Jose')
|
||||
end
|
||||
|
||||
def expect_message_content
|
||||
@@ -513,4 +514,12 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do
|
||||
def expect_message_has_attachment
|
||||
expect(whatsapp_channel.inbox.messages.first.attachments.present?).to be true
|
||||
end
|
||||
|
||||
def contact_phone_number
|
||||
"+#{sender_number}"
|
||||
end
|
||||
|
||||
def contact_from_number
|
||||
Contact.find_by(phone_number: contact_phone_number)
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user