# Pull Request Template ## Description Fixes: https://github.com/chatwoot/chatwoot/issues/13880 Uses approaches discussed from: https://github.com/chatwoot/chatwoot/pull/13883 Activity messages pertaining to resolve are included along with an instruction for the LLM to choose whether to consider them or not along ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. locally and with specs ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Sony Mathew <sony@chatwoot.com>
301 lines
14 KiB
Ruby
301 lines
14 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe '/api/v1/widget/messages', type: :request do
|
|
let(:account) { create(:account) }
|
|
let(:web_widget) { create(:channel_widget, account: account) }
|
|
let(:contact) { create(:contact, account: account, email: nil) }
|
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
|
|
let(:conversation) { create(:conversation, contact: contact, account: account, inbox: web_widget.inbox, contact_inbox: contact_inbox) }
|
|
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
|
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
|
|
|
|
before do |example|
|
|
2.times.each { create(:message, account: account, inbox: web_widget.inbox, conversation: conversation) } unless example.metadata[:skip_before]
|
|
end
|
|
|
|
describe 'GET /api/v1/widget/messages' do
|
|
context 'when get request is made' do
|
|
it 'returns messages in conversation' do
|
|
get api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
# 2 messages created + 2 messages by the email hook
|
|
expect(json_response['payload'].length).to eq(4)
|
|
expect(json_response['meta']).not_to be_empty
|
|
end
|
|
|
|
it 'returns empty messages', :skip_before do
|
|
get api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
expect(json_response['payload'].length).to eq(0)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/widget/messages' do
|
|
context 'when post request is made' do
|
|
it 'creates message in conversation' do
|
|
conversation.destroy! # Test all params
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
expect(json_response['content']).to eq(message_params[:content])
|
|
end
|
|
|
|
it 'creates conversation with custom_attributes when first message is sent' do
|
|
conversation.destroy!
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
custom_attributes = { plan: 'enterprise', source: 'website' }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params, custom_attributes: custom_attributes },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
new_conversation = contact.conversations.last
|
|
expect(new_conversation.custom_attributes).to include('plan' => 'enterprise', 'source' => 'website')
|
|
end
|
|
|
|
it 'creates conversation with labels when first message is sent' do
|
|
conversation.destroy!
|
|
label = create(:label, title: 'vip', account: account)
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params, labels: [label.title] },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
new_conversation = contact.conversations.last
|
|
expect(new_conversation.label_list).to include('vip')
|
|
end
|
|
|
|
it 'ignores invalid labels when creating conversation with first message' do
|
|
conversation.destroy!
|
|
create(:label, title: 'valid-label', account: account)
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params, labels: %w[valid-label nonexistent] },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
new_conversation = contact.conversations.last
|
|
expect(new_conversation.label_list).to include('valid-label')
|
|
expect(new_conversation.label_list).not_to include('nonexistent')
|
|
end
|
|
|
|
it 'does not apply labels or custom_attributes when conversation already exists' do
|
|
create(:label, title: 'vip', account: account)
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
custom_attributes = { plan: 'enterprise' }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params,
|
|
custom_attributes: custom_attributes, labels: ['vip'] },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
conversation.reload
|
|
expect(conversation.custom_attributes).not_to include('plan' => 'enterprise')
|
|
expect(conversation.label_list).not_to include('vip')
|
|
end
|
|
|
|
it 'does not create the message' do
|
|
conversation.destroy! # Test all params
|
|
message_params = { content: "#{'h' * 150 * 1000}a", timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
json_response = response.parsed_body
|
|
|
|
expect(json_response['message']).to eq('Content is too long (maximum is 150000 characters)')
|
|
end
|
|
|
|
it 'creates message in conversation with a valid reply to' do
|
|
message_params = { content: 'hello world reply', timestamp: Time.current, reply_to: conversation.messages.first.id }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
expect(json_response['content']).to eq(message_params[:content])
|
|
expect(json_response['content_attributes']['in_reply_to']).to eq(conversation.messages.first.id)
|
|
# check nil for external id since this is a web widget conversation
|
|
expect(json_response['content_attributes']['in_reply_to_external_id']).to be_nil
|
|
end
|
|
|
|
it 'creates message in conversation with an in-valid reply to' do
|
|
message_params = { content: 'hello world reply', timestamp: Time.current, reply_to: conversation.messages.first.id + 300 }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
expect(json_response['content']).to eq(message_params[:content])
|
|
expect(json_response['content_attributes']['in_reply_to']).to be_nil
|
|
expect(json_response['content_attributes']['in_reply_to_external_id']).to be_nil
|
|
end
|
|
|
|
it 'creates attachment message in conversation' do
|
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
|
message_params = { content: 'hello world', timestamp: Time.current, attachments: [file] }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token }
|
|
|
|
expect(response).to have_http_status(:success)
|
|
json_response = response.parsed_body
|
|
expect(json_response['content']).to eq(message_params[:content])
|
|
|
|
expect(conversation.messages.last.attachments.first.file.present?).to be(true)
|
|
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
|
|
end
|
|
|
|
it 'does not reopen conversation when conversation is muted' do
|
|
conversation.mute!
|
|
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.reload.resolved?).to be(true)
|
|
end
|
|
|
|
it 'does not create resolved activity messages when snoozed conversation is opened' do
|
|
conversation.snoozed!
|
|
|
|
message_params = { content: 'hello world', timestamp: Time.current }
|
|
post api_v1_widget_messages_url,
|
|
params: { website_token: web_widget.website_token, message: message_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(Conversations::ActivityMessageJob).not_to have_been_enqueued.at_least(:once).with(
|
|
conversation,
|
|
{
|
|
account_id: conversation.account_id,
|
|
inbox_id: conversation.inbox_id,
|
|
message_type: :activity,
|
|
content: "Conversation was resolved by #{contact.name}",
|
|
content_attributes: { activity: { type: 'conversation_status_changed', status: 'resolved' } }
|
|
}
|
|
)
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.reload.open?).to be(true)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'PUT /api/v1/widget/messages' do
|
|
context 'when put request targets a message from another visitor in the same inbox' do
|
|
it 'does not update the foreign message' do
|
|
other_contact = create(:contact, account: account, email: nil)
|
|
other_contact_inbox = create(:contact_inbox, contact: other_contact, inbox: web_widget.inbox)
|
|
other_conversation = create(:conversation, contact: other_contact, account: account,
|
|
inbox: web_widget.inbox, contact_inbox: other_contact_inbox)
|
|
foreign_message = create(:message, content_type: 'input_email', account: account,
|
|
inbox: web_widget.inbox, conversation: other_conversation)
|
|
original_email = foreign_message.submitted_email
|
|
|
|
put api_v1_widget_message_url(foreign_message.id),
|
|
params: { website_token: web_widget.website_token, contact: { email: Faker::Internet.email } },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:not_found)
|
|
expect(foreign_message.reload.submitted_email).to eq(original_email)
|
|
end
|
|
end
|
|
|
|
context 'when put request is made with non existing email' do
|
|
it 'updates message in conversation and creates a new contact' do
|
|
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
|
email = Faker::Internet.email
|
|
contact_params = { email: email }
|
|
put api_v1_widget_message_url(message.id),
|
|
params: { website_token: web_widget.website_token, contact: contact_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
message.reload
|
|
expect(message.submitted_email).to eq(email)
|
|
expect(message.conversation.contact.email).to eq(email)
|
|
expect(message.conversation.contact.name).to eq(email.split('@')[0])
|
|
end
|
|
end
|
|
|
|
context 'when put request is made with invalid email' do
|
|
it 'rescues the error' do
|
|
message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
|
|
contact_params = { email: nil }
|
|
put api_v1_widget_message_url(message.id),
|
|
params: { website_token: web_widget.website_token, contact: contact_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
end
|
|
|
|
context 'when put request is made with existing email' do
|
|
it 'updates message in conversation and deletes the current contact' do
|
|
message = create(:message, account: account, content_type: 'input_email', inbox: web_widget.inbox, conversation: conversation)
|
|
email = Faker::Internet.email
|
|
existing_contact = create(:contact, account: account, email: email, name: 'John Doe')
|
|
contact_params = { email: email }
|
|
put api_v1_widget_message_url(message.id),
|
|
params: { website_token: web_widget.website_token, contact: contact_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
message.reload
|
|
expect(existing_contact.reload.name).to eq('John Doe')
|
|
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
|
|
it 'ignores the casing of email, updates message in conversation and deletes the current contact' do
|
|
message = create(:message, content_type: 'input_email', account: account, inbox: web_widget.inbox, conversation: conversation)
|
|
email = Faker::Internet.email
|
|
create(:contact, account: account, email: email)
|
|
contact_params = { email: email.upcase }
|
|
put api_v1_widget_message_url(message.id),
|
|
params: { website_token: web_widget.website_token, contact: contact_params },
|
|
headers: { 'X-Auth-Token' => token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
message.reload
|
|
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
end
|
|
end
|
|
end
|
|
end
|