* feat: Adds model for scheduling messages * feat: Implement scheduled message handling and processing jobs * feat: Add ScheduledMessagesController and associated specs for managing scheduled messages * refactor: Simplify scheduled message job specs and improve metadata handling * feat: Add ScheduledMessagePolicy for managing access to scheduled messages * feat: Add routes for managing scheduled messages * feat: Add scheduled message event handling and broadcasting * feat: Add JSON views for scheduled messages creation, destruction, updating, and indexing * feat: Update scheduled message status and dispatch update event after message creation * feat: Ensure scheduled message updates trigger dispatch event * feat: Add mutation types for managing scheduled messages * feat: Add additionalAttributes prop to Message component and provider * feat: Implement scheduled message handling in ActionCable and Vuex store * feat: Add unit tests for scheduled messages actions and mutations * feat: implement scheduled messages functionality - Added support for scheduling messages in the conversation dashboard. - Introduced new components: ScheduledMessageModal and ScheduledMessages for managing scheduled messages. - Enhanced ReplyBottomPanel to include scheduling options. - Updated Base.vue to handle scheduled message styling. - Integrated Vuex store module for managing scheduled messages state. - Added necessary translations for scheduled messages in English and Portuguese. * feat: add pagination to scheduled messages index and update tests accordingly * chore: update scheduled messages specs for future time validation and response status * chore: enhance scheduled messages API with pagination and add skeleton loader component * feat: add create_scheduled_message action to automation rule attributes * feat: implement create_scheduled_message action and enhance attachment handling * feat: add scheduled message functionality with UI components and localization * test: enhance scheduledMessages mutations tests with meta handling and structure * chore: update label to display file name upon successful upload in AutomationFileInput component * feat: add initialAttachment prop to ScheduledMessageModal and update ReplyBox to pass attachment * chore: prepend_mod_with to ScheduledMessagesController for better module handling * fix: attachment visibility in ScheduledMessageItem component * chore: enhance ScheduledMessage model with validations and reduce controller load * refactor: simplify ScheduledMessagesAPI methods by removing unnecessary instance variable * chore: update event emission for scheduled message creation in ReplyBox and ScheduledMessageModal * refactor: update status configuration to use label keys * chore: update date formatting in ScheduledMessageItem component * refactor: collapse logic to checkOverflow and update related functionality * chore: add author indication for current user in scheduled messages * chore: enhance scheduled message metadata with author information and localization * fix: send message shortcut * chore: handle errors in scheduled message submission * chore: update scheduled message modal to use combined date and time input * chore: refactor scheduled messages handling to remove pagination and update related tests * fix: ensure scheduled messages update status and dispatch on failure * fix: update scheduled message due date logic and simplify sending checks * refactor: rename build_message method for send_message * fix: update scheduled message creation time and improve test reliability * chore: ignore unnecessary check * chore: add scheduled message metadata handling in message builder, add scheduled message factorie and update specs * refactor: use scheduled message factorie creation in specs * chore: streamline error handling in scheduled message job and remove dispatch logic * fix: change scheduled_messages association to destroy dependent records * refactor: remove unused attributes from scheduled message payload builder * chore: update scheduled message retrieval to use conversation association * chore: correct cron format for scheduled messages job * chore: remove migration for author_type in scheduled_messages * feat: enhance scheduled messages management with delete confirmation and error handling * chore: set cron poll interval to 10 seconds for improved scheduling precision * feat: include additional_attributes in message JSON response * feat: enhance scheduled message validation and localization support * chore: update scheduled message display * Merge branch 'main' into Cayo-Oliveira/CU-86aenh268/Mensagens-agendadas * feat: add scheduled message indicators and validation for message length * fix: remove unnecessary condition from line-clamp class binding * feat: update scheduled messages localization and enhance content validation * feat: update scheduled messages order, enhance scheduledAt computation, and add message association * fix: reorder condition for Facebook channel message length computation * fix: change detection for attachments in scheduled messages * fix: remove unnecessary colon from close-on-backdrop-click prop in ScheduledMessageModal * chore: add error handling for scheduled message deletion and update localization for delete failure * fix: enforce minimum delay of 1 minute for scheduled messages and update validation * fix: remove unused private property and improve locale formatting for scheduled messages * fix: adjust positioning of DropdownBody in ReplyBottomPanel and clean up schema foreign keys * docs: add scheduled messages management APIs and payload definitions --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
385 lines
16 KiB
Ruby
385 lines
16 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe Messages::MessageBuilder do
|
|
subject(:message_builder) { described_class.new(user, conversation, params).perform }
|
|
|
|
let(:account) { create(:account) }
|
|
let(:user) { create(:user, account: account) }
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) }
|
|
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
|
let(:message_for_reply) { create(:message, conversation: conversation) }
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test'
|
|
})
|
|
end
|
|
|
|
describe '#perform' do
|
|
it 'creates a message' do
|
|
message = message_builder
|
|
expect(message.content).to eq params[:content]
|
|
end
|
|
end
|
|
|
|
describe '#content_attributes' do
|
|
context 'when content_attributes is a JSON string' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
content_attributes: "{\"in_reply_to\":#{message_for_reply.id}}"
|
|
})
|
|
end
|
|
|
|
it 'parses content_attributes from JSON string' do
|
|
message = described_class.new(user, conversation, params).perform
|
|
expect(message.content_attributes).to include(in_reply_to: message_for_reply.id)
|
|
end
|
|
end
|
|
|
|
context 'when content_attributes is a hash' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
content_attributes: { in_reply_to: message_for_reply.id }
|
|
})
|
|
end
|
|
|
|
it 'uses content_attributes as provided' do
|
|
message = described_class.new(user, conversation, params).perform
|
|
expect(message.content_attributes).to include(in_reply_to: message_for_reply.id)
|
|
end
|
|
end
|
|
|
|
context 'when content_attributes is absent' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({ content: 'test' })
|
|
end
|
|
|
|
it 'defaults to an empty hash' do
|
|
message = message_builder
|
|
expect(message.content_attributes).to eq({})
|
|
end
|
|
end
|
|
|
|
context 'when content_attributes is nil' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
content_attributes: nil
|
|
})
|
|
end
|
|
|
|
it 'defaults to an empty hash' do
|
|
message = message_builder
|
|
expect(message.content_attributes).to eq({})
|
|
end
|
|
end
|
|
|
|
context 'when content_attributes is an invalid JSON string' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
content_attributes: 'invalid_json'
|
|
})
|
|
end
|
|
|
|
it 'defaults to an empty hash' do
|
|
message = message_builder
|
|
expect(message.content_attributes).to eq({})
|
|
end
|
|
end
|
|
end
|
|
|
|
describe '#perform when message_type is incoming' do
|
|
context 'when channel is not api' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
message_type: 'incoming'
|
|
})
|
|
end
|
|
|
|
it 'creates throws error when channel is not api' do
|
|
expect { message_builder }.to raise_error 'Incoming messages are only allowed in Api inboxes'
|
|
end
|
|
end
|
|
|
|
context 'when channel is api' do
|
|
let(:channel_api) { create(:channel_api, account: account) }
|
|
let(:conversation) { create(:conversation, inbox: channel_api.inbox, account: account) }
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
message_type: 'incoming'
|
|
})
|
|
end
|
|
|
|
it 'creates message when channel is api' do
|
|
message = message_builder
|
|
expect(message.message_type).to eq params[:message_type]
|
|
end
|
|
end
|
|
|
|
context 'when attachment messages' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
attachments: [Rack::Test::UploadedFile.new('spec/assets/avatar.png', 'image/png')]
|
|
})
|
|
end
|
|
|
|
it 'creates message with attachments' do
|
|
message = message_builder
|
|
expect(message.attachments.first.file_type).to eq 'image'
|
|
end
|
|
|
|
it 'creates attachment with is_recorded_audio metadata' do
|
|
params[:is_recorded_audio] = true
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to eq({ 'is_recorded_audio' => true })
|
|
end
|
|
|
|
it 'creates attachment with is_recorded_audio metadata when param is array of filenames' do
|
|
params[:is_recorded_audio] = ['avatar.png']
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to eq({ 'is_recorded_audio' => true })
|
|
end
|
|
|
|
it 'creates attachment with is_recorded_audio metadata when param is string with array' do
|
|
params[:is_recorded_audio] = '["avatar.png"]'
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to eq({ 'is_recorded_audio' => true })
|
|
end
|
|
|
|
it 'creates attachment with custom metadata from attachments_metadata param' do
|
|
params[:attachments_metadata] = { 'avatar.png' => { description: 'Profile picture', source: 'upload' } }
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to include('description' => 'Profile picture', 'source' => 'upload')
|
|
end
|
|
|
|
it 'does not apply metadata when filename key does not match' do
|
|
params[:attachments_metadata] = { 'other_file.png' => { description: 'Wrong file' } }
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to be_nil
|
|
end
|
|
|
|
it 'merges is_recorded_audio with attachments_metadata' do
|
|
params[:is_recorded_audio] = true
|
|
params[:attachments_metadata] = { 'avatar.png' => { description: 'Audio note' } }
|
|
|
|
message = message_builder
|
|
|
|
expect(message.attachments.first.meta).to eq({
|
|
'is_recorded_audio' => true,
|
|
'description' => 'Audio note'
|
|
})
|
|
end
|
|
|
|
context 'when DIRECT_UPLOAD_ENABLED' do
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
attachments: [get_blob_for('spec/assets/avatar.png', 'image/png').signed_id]
|
|
})
|
|
end
|
|
|
|
it 'creates message with attachments' do
|
|
message = message_builder
|
|
expect(message.attachments.first.file_type).to eq 'image'
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when email channel messages' do
|
|
let!(:channel_email) { create(:channel_email, account: account) }
|
|
let(:inbox_member) { create(:inbox_member, inbox: channel_email.inbox) }
|
|
let(:conversation) { create(:conversation, inbox: channel_email.inbox, account: account) }
|
|
let(:params) do
|
|
ActionController::Parameters.new({ cc_emails: 'test_cc_mail@test.com', bcc_emails: 'test_bcc_mail@test.com' })
|
|
end
|
|
|
|
it 'creates message with content_attributes for cc and bcc email addresses' do
|
|
message = message_builder
|
|
|
|
expect(message.content_attributes[:cc_emails]).to eq [params[:cc_emails]]
|
|
expect(message.content_attributes[:bcc_emails]).to eq [params[:bcc_emails]]
|
|
end
|
|
|
|
it 'does not create message with wrong cc and bcc email addresses' do
|
|
params = ActionController::Parameters.new({ cc_emails: 'test.com', bcc_emails: 'test_bcc.com' })
|
|
expect { described_class.new(user, conversation, params).perform }.to raise_error 'Invalid email address'
|
|
end
|
|
|
|
it 'strips off whitespace before saving cc_emails and bcc_emails' do
|
|
cc_emails = ' test1@test.com , test2@test.com, test3@test.com'
|
|
bcc_emails = 'test1@test.com,test2@test.com, test3@test.com '
|
|
params = ActionController::Parameters.new({ cc_emails: cc_emails, bcc_emails: bcc_emails })
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes[:cc_emails]).to eq ['test1@test.com', 'test2@test.com', 'test3@test.com']
|
|
expect(message.content_attributes[:bcc_emails]).to eq ['test1@test.com', 'test2@test.com', 'test3@test.com']
|
|
end
|
|
|
|
context 'when custom email content is provided' do
|
|
before do
|
|
account.enable_features('quoted_email_reply')
|
|
end
|
|
|
|
it 'creates message with custom HTML email content' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Regular message content',
|
|
email_html_content: '<p>Custom <strong>HTML</strong> content</p>'
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'html_content', 'full')).to eq '<p>Custom <strong>HTML</strong> content</p>'
|
|
expect(message.content_attributes.dig('email', 'html_content', 'reply')).to eq '<p>Custom <strong>HTML</strong> content</p>'
|
|
expect(message.content_attributes.dig('email', 'text_content', 'full')).to eq 'Regular message content'
|
|
expect(message.content_attributes.dig('email', 'text_content', 'reply')).to eq 'Regular message content'
|
|
end
|
|
|
|
it 'does not process custom email content for private messages' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Regular message content',
|
|
email_html_content: '<p>Custom HTML content</p>',
|
|
private: true
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'html_content')).to be_nil
|
|
expect(message.content_attributes.dig('email', 'text_content')).to be_nil
|
|
end
|
|
|
|
it 'falls back to default behavior when no custom email content is provided' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Regular **markdown** content'
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'html_content', 'full')).to include('<strong>markdown</strong>')
|
|
expect(message.content_attributes.dig('email', 'text_content', 'full')).to eq 'Regular **markdown** content'
|
|
end
|
|
end
|
|
|
|
context 'when liquid templates are present in email content' do
|
|
let(:contact) { create(:contact, name: 'John', email: 'john@example.com') }
|
|
let(:conversation) { create(:conversation, inbox: channel_email.inbox, account: account, contact: contact) }
|
|
|
|
it 'processes liquid variables in email content' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Hello {{contact.name}}, your email is {{contact.email}}'
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'html_content', 'full')).to include('Hello John')
|
|
expect(message.content_attributes.dig('email', 'html_content', 'full')).to include('john@example.com')
|
|
expect(message.content_attributes.dig('email', 'text_content', 'full')).to eq 'Hello John, your email is john@example.com'
|
|
end
|
|
|
|
it 'does not process liquid in code blocks' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Hello {{contact.name}}, use this code: `{{contact.email}}`'
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'text_content', 'full')).to eq 'Hello John, use this code: `{{contact.email}}`'
|
|
end
|
|
|
|
it 'handles broken liquid syntax gracefully' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Hello {{contact.name} {{invalid}}'
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'text_content', 'full')).to eq 'Hello {{contact.name} {{invalid}}'
|
|
end
|
|
|
|
it 'does not process liquid for incoming messages' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Hello {{contact.name}}',
|
|
message_type: 'incoming'
|
|
})
|
|
|
|
api_channel = create(:channel_api, account: account)
|
|
api_conversation = create(:conversation, inbox: api_channel.inbox, account: account, contact: contact)
|
|
|
|
message = described_class.new(user, api_conversation, params).perform
|
|
|
|
expect(message.content).to eq 'Hello {{contact.name}}'
|
|
end
|
|
|
|
it 'does not process liquid for private messages' do
|
|
params = ActionController::Parameters.new({
|
|
content: 'Hello {{contact.name}}',
|
|
private: true
|
|
})
|
|
|
|
message = described_class.new(user, conversation, params).perform
|
|
|
|
expect(message.content_attributes.dig('email', 'html_content')).to be_nil
|
|
expect(message.content_attributes.dig('email', 'text_content')).to be_nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'scheduled_message metadata' do
|
|
let(:scheduled_message) { create(:scheduled_message, account: account, inbox: inbox, conversation: conversation, author: user, content: 'Hello') }
|
|
let(:params) do
|
|
ActionController::Parameters.new({
|
|
content: 'test',
|
|
scheduled_message: scheduled_message
|
|
})
|
|
end
|
|
|
|
it 'includes scheduled_message_id in additional_attributes' do
|
|
message = message_builder
|
|
|
|
expect(message.additional_attributes['scheduled_message_id']).to eq(scheduled_message.id)
|
|
end
|
|
|
|
it 'includes scheduled_by with author info' do
|
|
message = message_builder
|
|
|
|
expect(message.additional_attributes['scheduled_by']).to include('id' => user.id, 'type' => 'User', 'name' => user.name)
|
|
end
|
|
|
|
it 'includes scheduled_at timestamp' do
|
|
message = message_builder
|
|
|
|
expect(message.additional_attributes['scheduled_at']).to eq(scheduled_message.updated_at.to_i)
|
|
end
|
|
|
|
context 'when author is AutomationRule' do
|
|
let(:automation_rule) { create(:automation_rule, account: account) }
|
|
let(:scheduled_message) do
|
|
create(:scheduled_message, account: account, inbox: inbox, conversation: conversation, author: automation_rule, content: 'Hello')
|
|
end
|
|
|
|
it 'includes scheduled_by with automation_rule info' do
|
|
message = message_builder
|
|
|
|
expect(message.additional_attributes['scheduled_by']).to include('id' => automation_rule.id, 'type' => 'AutomationRule')
|
|
end
|
|
end
|
|
end
|
|
end
|