# 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>
384 lines
16 KiB
Ruby
384 lines
16 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe 'Conversation Messages API', type: :request do
|
|
let!(:account) { create(:account) }
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/messages' do
|
|
let!(:inbox) { create(:inbox, account: account) }
|
|
let!(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id)
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with access to conversation' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'creates a new outgoing message' do
|
|
params = { content: 'test-message', private: true }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(conversation.messages.count).to eq(1)
|
|
expect(conversation.messages.first.content).to eq(params[:content])
|
|
end
|
|
|
|
it 'does not create the message' do
|
|
params = { content: "#{'h' * 150 * 1000}a", private: true }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
json_response = response.parsed_body
|
|
|
|
expect(json_response['error']).to eq('Validation failed: Content is too long (maximum is 150000 characters)')
|
|
end
|
|
|
|
it 'returns a customer-safe error when the database query is canceled' do
|
|
message_builder = instance_double(Messages::MessageBuilder)
|
|
allow(Messages::MessageBuilder).to receive(:new).and_return(message_builder)
|
|
allow(message_builder).to receive(:perform)
|
|
.and_raise(ActiveRecord::QueryCanceled, 'PG::QueryCanceled: ERROR: canceling statement due to statement timeout')
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: { content: 'test-message', private: true },
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['error']).to eq(I18n.t('errors.database.query_canceled'))
|
|
expect(response.parsed_body['error']).not_to include('PG::QueryCanceled')
|
|
end
|
|
|
|
it 'creates an outgoing text message with a specific bot sender' do
|
|
agent_bot = create(:agent_bot)
|
|
time_stamp = Time.now.utc.to_s
|
|
params = { content: 'test-message', external_created_at: time_stamp, sender_type: 'AgentBot', sender_id: agent_bot.id }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_data = response.parsed_body
|
|
expect(response_data['content_attributes']['external_created_at']).to eq time_stamp
|
|
expect(conversation.messages.count).to eq(1)
|
|
expect(conversation.messages.last.sender_id).to eq(agent_bot.id)
|
|
expect(conversation.messages.last.content_type).to eq('text')
|
|
end
|
|
|
|
it 'creates a new outgoing message with attachment' do
|
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
|
params = { content: 'test-message', attachments: [file] }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: agent.create_new_auth_token
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.messages.last.attachments.first.file.present?).to be(true)
|
|
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
|
|
end
|
|
|
|
context 'when api inbox' do
|
|
let(:api_channel) { create(:channel_api, account: account) }
|
|
let(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
|
|
let(:conversation) { create(:conversation, inbox: api_inbox, account: account) }
|
|
|
|
it 'reopens the conversation with new incoming message' do
|
|
create(:message, conversation: conversation, account: account)
|
|
conversation.resolved!
|
|
|
|
params = { content: 'test-message', private: false, message_type: 'incoming' }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.reload.status).to eq('open')
|
|
expect(Conversations::ActivityMessageJob)
|
|
.to(have_been_enqueued.at_least(:once)
|
|
.with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
|
content: 'System reopened the conversation due to a new incoming message.',
|
|
content_attributes: {
|
|
activity: {
|
|
type: 'conversation_status_changed',
|
|
status: 'open'
|
|
}
|
|
} }))
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated agent bot' do
|
|
let!(:agent_bot) { create(:agent_bot) }
|
|
|
|
it 'creates a new outgoing message' do
|
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
|
params = { content: 'test-message' }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: { api_access_token: agent_bot.access_token.token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.messages.count).to eq(1)
|
|
expect(conversation.messages.first.content).to eq(params[:content])
|
|
end
|
|
|
|
it 'creates a new outgoing input select message' do
|
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
|
select_item1 = build(:bot_message_select)
|
|
select_item2 = build(:bot_message_select)
|
|
params = { content_type: 'input_select', content_attributes: { items: [select_item1, select_item2] } }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: { api_access_token: agent_bot.access_token.token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.messages.count).to eq(1)
|
|
expect(conversation.messages.first.content_type).to eq(params[:content_type])
|
|
expect(conversation.messages.first.content).to be_nil
|
|
end
|
|
|
|
it 'creates a new outgoing cards message' do
|
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
|
card = build(:bot_message_card)
|
|
params = { content_type: 'cards', content_attributes: { items: [card] } }
|
|
|
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
|
params: params,
|
|
headers: { api_access_token: agent_bot.access_token.token },
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(conversation.messages.count).to eq(1)
|
|
expect(conversation.messages.first.content_type).to eq(params[:content_type])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/messages' do
|
|
let(:conversation) { create(:conversation, account: account) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with access to conversation' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'shows the conversation' do
|
|
get "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:contact][:id]).to eq(conversation.contact_id)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id' do
|
|
let(:message) { create(:message, account: account, content_attributes: { bcc_emails: ['hello@chatwoot.com'] }) }
|
|
let(:conversation) { message.conversation }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message.id}"
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with access to conversation' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'deletes the message' do
|
|
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{message.id}",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(message.reload.content).to eq 'This message was deleted'
|
|
expect(message.reload.deleted).to be true
|
|
expect(message.reload.content_attributes['bcc_emails']).to be_nil
|
|
end
|
|
|
|
it 'deletes interactive messages' do
|
|
interactive_message = create(
|
|
:message, message_type: :outgoing, content: 'test', content_type: 'input_select',
|
|
content_attributes: { 'items' => [{ 'title' => 'test', 'value' => 'test' }] },
|
|
conversation: conversation
|
|
)
|
|
|
|
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/#{interactive_message.id}",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(interactive_message.reload.deleted).to be true
|
|
end
|
|
end
|
|
|
|
context 'when the message id is invalid' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'returns not found error' do
|
|
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/messages/99999",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id/retry' do
|
|
let(:message) { create(:message, account: account, status: :failed, content_attributes: { external_error: 'error' }) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry"
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with access to conversation' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: message.conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'retries the message' do
|
|
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/#{message.id}/retry",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(message.reload.status).to eq('sent')
|
|
expect(message.reload.content_attributes['external_error']).to be_nil
|
|
end
|
|
end
|
|
|
|
context 'when the message id is invalid' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
before do
|
|
create(:inbox_member, inbox: message.conversation.inbox, user: agent)
|
|
end
|
|
|
|
it 'returns not found error' do
|
|
post "/api/v1/accounts/#{account.id}/conversations/#{message.conversation.display_id}/messages/99999/retry",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'PATCH /api/v1/accounts/{account.id}/conversations/:conversation_id/messages/:id' do
|
|
let(:api_channel) { create(:channel_api, account: account) }
|
|
let(:api_inbox) { create(:inbox, channel: api_channel, account: account) }
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
let!(:conversation) { create(:conversation, inbox: api_inbox, account: account) }
|
|
let!(:message) { create(:message, conversation: conversation, account: account, status: :sent) }
|
|
|
|
context 'when unauthenticated' do
|
|
it 'returns unauthorized' do
|
|
patch api_v1_account_conversation_message_url(account_id: account.id, conversation_id: conversation.display_id, id: message.id)
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when authenticated agent' do
|
|
context 'when agent has non-API inbox' do
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
let!(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
|
|
|
before { create(:inbox_member, inbox: inbox, user: agent) }
|
|
|
|
it 'returns forbidden' do
|
|
patch api_v1_account_conversation_message_url(
|
|
account_id: account.id,
|
|
conversation_id: conversation.display_id,
|
|
id: message.id
|
|
), params: { status: 'failed', external_error: 'err' }, headers: agent.create_new_auth_token, as: :json
|
|
expect(response).to have_http_status(:forbidden)
|
|
end
|
|
end
|
|
|
|
context 'when agent has API inbox' do
|
|
before { create(:inbox_member, inbox: api_inbox, user: agent) }
|
|
|
|
it 'uses StatusUpdateService to perform status update' do
|
|
service = instance_double(Messages::StatusUpdateService)
|
|
expect(Messages::StatusUpdateService).to receive(:new)
|
|
.with(message, 'failed', 'err123')
|
|
.and_return(service)
|
|
expect(service).to receive(:perform)
|
|
patch api_v1_account_conversation_message_url(
|
|
account_id: account.id,
|
|
conversation_id: conversation.display_id,
|
|
id: message.id
|
|
), params: { status: 'failed', external_error: 'err123' }, headers: agent.create_new_auth_token, as: :json
|
|
end
|
|
|
|
it 'updates status to failed with external_error' do
|
|
patch api_v1_account_conversation_message_url(
|
|
account_id: account.id,
|
|
conversation_id: conversation.display_id,
|
|
id: message.id
|
|
), params: { status: 'failed', external_error: 'err123' }, headers: agent.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(message.reload.status).to eq('failed')
|
|
expect(message.reload.external_error).to eq('err123')
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|