Merge branch 'develop' into codex/agent-bot-assignment-status

This commit is contained in:
Sojan Jose
2026-07-15 21:03:01 -07:00
committed by GitHub
390 changed files with 20011 additions and 568 deletions
@@ -15,7 +15,7 @@ RSpec.describe 'Agent Bot API', type: :request do
end
end
context 'when it is an authenticated user' do
context 'when it is an authenticated agent' do
it 'returns all the agent_bots in account along with global agent bots' do
global_bot = create(:agent_bot)
get "/api/v1/accounts/#{account.id}/agent_bots",
@@ -25,7 +25,7 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.name)
expect(response.body).to include(global_bot.name)
expect(response.body).to include(agent_bot.access_token.token)
expect(response.body).not_to include(agent_bot.access_token.token)
expect(response.body).not_to include(global_bot.access_token.token)
end
@@ -54,6 +54,17 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(account_bot_response).to include('thumbnail')
end
end
context 'when it is an authenticated administrator' do
it 'returns the account bot access token' do
get "/api/v1/accounts/#{account.id}/agent_bots",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.access_token.token)
end
end
end
describe 'GET /api/v1/accounts/{account.id}/agent_bots/:id' do
@@ -65,7 +76,7 @@ RSpec.describe 'Agent Bot API', type: :request do
end
end
context 'when it is an authenticated user' do
context 'when it is an authenticated agent' do
it 'shows the agent bot' do
get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: agent.create_new_auth_token,
@@ -73,7 +84,7 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.name)
expect(response.body).to include(agent_bot.access_token.token)
expect(response.body).not_to include(agent_bot.access_token.token)
end
it 'will show a global agent bot' do
@@ -91,6 +102,17 @@ RSpec.describe 'Agent Bot API', type: :request do
expect(response.parsed_body).not_to include('outgoing_url')
end
end
context 'when it is an authenticated administrator' do
it 'returns the account bot access token' do
get "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(agent_bot.access_token.token)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/agent_bots' do
@@ -62,6 +62,24 @@ RSpec.describe 'Assignable Agents API', type: :request do
expect(response_data.size).to eq(2)
expect(response_data.pluck(:role)).to include('agent', 'administrator')
end
context 'with Agent Bots' do
let!(:account_bot) { create(:agent_bot, account: account, name: 'Account bot') }
let!(:global_bot) { create(:agent_bot, account: nil, name: 'Global bot') }
it 'returns assignable agents and accessible agent bots' do
get "/api/v1/accounts/#{account.id}/assignable_agents",
params: { inbox_ids: [inbox1.id, inbox2.id], include_agent_bots: true },
headers: agent1.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
response_data = response.parsed_body['payload']
expect(response_data.pluck('assignee_type')).to include('User', 'AgentBot')
expect(response_data.pluck('name')).to include(agent1.name, admin.name, account_bot.name, global_bot.name)
end
end
end
end
end
@@ -67,6 +67,50 @@ RSpec.describe 'Api::V1::Accounts::Captain::Preferences', type: :request do
source: 'default'
)
end
it 'returns the assistant YAML default for V1 accounts' do
get "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response.dig(:features, :assistant)).to include(
default: Llm::Models.default_model_for('assistant'),
selected: Llm::Models.default_model_for('assistant'),
source: 'default'
)
end
it 'returns GPT-5.2 as the assistant default for V2 accounts' do
account.enable_features!('captain_integration_v2')
get "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response.dig(:features, :assistant)).to include(
default: Llm::FeatureRouter::CAPTAIN_V2_ASSISTANT_MODEL,
selected: Llm::FeatureRouter::CAPTAIN_V2_ASSISTANT_MODEL,
source: 'default'
)
end
it 'keeps the V2 assistant default when an account override is selected' do
account.enable_features!('captain_integration_v2')
account.update!(captain_models: { 'assistant' => 'gpt-5.1' })
get "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(json_response.dig(:features, :assistant)).to include(
default: Llm::FeatureRouter::CAPTAIN_V2_ASSISTANT_MODEL,
selected: 'gpt-5.1',
source: 'account_override'
)
end
end
end
@@ -154,6 +198,17 @@ RSpec.describe 'Api::V1::Accounts::Captain::Preferences', type: :request do
expect(account.reload.captain_models['document_faq_generation']).to eq('gpt-5.2')
end
it 'updates captain_models for conversation FAQ generation' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
params: { captain_models: { conversation_faq_generation: 'gpt-4.1-mini' } },
as: :json
expect(response).to have_http_status(:success)
expect(json_response.dig(:features, :conversation_faq_generation, :selected)).to eq('gpt-4.1-mini')
expect(account.reload.captain_models['conversation_faq_generation']).to eq('gpt-4.1-mini')
end
it 'updates captain_models for PDF FAQ generation' do
put "/api/v1/accounts/#{account.id}/captain/preferences",
headers: admin.create_new_auth_token,
@@ -51,6 +51,22 @@ RSpec.describe 'Conversation Messages API', type: :request do
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
@@ -103,7 +119,13 @@ RSpec.describe 'Conversation Messages API', type: :request do
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: 'System reopened the conversation due to a new incoming message.',
content_attributes: {
activity: {
type: 'conversation_status_changed',
status: 'open'
}
} }))
end
end
end
@@ -68,6 +68,23 @@ RSpec.describe 'Conversation Participants API', type: :request do
expect(response.body).to include(participant.email)
expect(conversation.conversation_participants.count).to eq(1)
end
it 'notifies unread counts when a participant is added' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
params = { user_ids: [participant.id] }
post api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'conversation.unread_count_changed',
kind_of(ActiveSupport::TimeWithZone),
conversation: conversation
)
end
end
end
@@ -106,6 +123,25 @@ RSpec.describe 'Conversation Participants API', type: :request do
expect(response.body).to include(participant_to_be_added.email)
expect(conversation.conversation_participants.count).to eq(2)
end
it 'notifies unread counts when participant membership changes' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
params = { user_ids: [participant.id, participant_to_be_added.id] }
create(:conversation_participant, conversation: conversation, user: participant)
create(:conversation_participant, conversation: conversation, user: participant_to_be_removed)
put api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'conversation.unread_count_changed',
kind_of(ActiveSupport::TimeWithZone),
conversation: conversation
)
end
end
end
@@ -137,6 +173,24 @@ RSpec.describe 'Conversation Participants API', type: :request do
expect(response).to have_http_status(:success)
expect(conversation.conversation_participants.count).to eq(0)
end
it 'notifies unread counts when a participant is removed' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
params = { user_ids: [participant.id] }
create(:conversation_participant, conversation: conversation, user: participant)
delete api_v1_account_conversation_participants_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: agent.create_new_auth_token,
as: :json
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'conversation.unread_count_changed',
kind_of(ActiveSupport::TimeWithZone),
conversation: conversation
)
end
end
end
end
@@ -159,6 +159,28 @@ RSpec.describe 'Conversations API', type: :request do
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']['teams']).to eq(team.id.to_s => 1)
end
it 'returns filtered unread counts when the filtered count feature is enabled' do
account.enable_features!(:unread_count_for_filters)
allow(Conversations::UnreadCounts::FilteredCountInstrumentation).to receive(:summarize_request) do |**_attributes, &block|
block.call
end
mentioned = create_unread_conversation(account: account, inbox: visible_inbox)
create(:mention, account: account, conversation: mentioned, user: agent)
get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(response.parsed_body['payload']).to include(
'mentions_count' => 1,
'participating_count' => 0,
'unattended_count' => 1,
'folders' => {}
)
expect(Conversations::UnreadCounts::FilteredCountInstrumentation).to have_received(:summarize_request).with(account_id: account.id)
end
end
it 'returns forbidden when conversation unread counts feature is disabled' do
@@ -865,6 +887,59 @@ RSpec.describe 'Conversations API', type: :request do
Conversations::UnreadCounts::Store.clear_account!(account.id)
end
it 'refreshes unread count cache before invalidating filtered counts when conversation is marked read' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
conversation.update!(agent_last_seen_at: 1.hour.ago)
create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming, created_at: 5.minutes.ago)
notifier = instance_double(Conversations::UnreadCounts::Notifier)
invalidator = instance_double(Conversations::UnreadCounts::FilteredCountInvalidator)
allow(Conversations::UnreadCounts::Notifier).to receive(:new).with(conversation).and_return(notifier)
allow(Conversations::UnreadCounts::FilteredCountInvalidator).to receive(:new).with(account).and_return(invalidator)
expect(notifier).to receive(:perform).ordered.and_return(true)
expect(invalidator).to receive(:conversation_changed!).ordered.and_return(true)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
it 'invalidates filtered unread counts when conversation is marked read' do
conversation.update!(agent_last_seen_at: 1.hour.ago)
create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming, created_at: 5.minutes.ago)
account.enable_features!(:unread_count_for_filters)
expect do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json
end.to change { Conversations::UnreadCounts::FilteredCountStore.conversation_version(account.id) }.by(1)
expect(response).to have_http_status(:success)
end
it 'notifies clients when marking read only affects filtered counts' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
conversation.update!(agent_last_seen_at: 1.hour.ago)
create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming, created_at: 5.minutes.ago)
allow(Conversations::UnreadCounts::Refresher).to receive(:new).and_return(
instance_double(Conversations::UnreadCounts::Refresher, perform: false)
)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'conversation.unread_count_changed',
kind_of(Time),
conversation: conversation
)
end
it 'updates both if one timestamp is old even when the other is recent' do
conversation.update!(assignee_id: agent.id, agent_last_seen_at: 2.hours.ago, assignee_last_seen_at: 30.minutes.ago)
# Ensure all messages are older than assignee_last_seen_at (no unread messages)
@@ -951,6 +1026,56 @@ RSpec.describe 'Conversations API', type: :request do
ensure
Conversations::UnreadCounts::Store.clear_account!(account.id)
end
it 'refreshes unread count cache before invalidating filtered counts when conversation is marked unread' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
conversation.update!(agent_last_seen_at: 1.minute.from_now, assignee_last_seen_at: 1.minute.from_now)
notifier = instance_double(Conversations::UnreadCounts::Notifier)
invalidator = instance_double(Conversations::UnreadCounts::FilteredCountInvalidator)
allow(Conversations::UnreadCounts::Notifier).to receive(:new).with(conversation).and_return(notifier)
allow(Conversations::UnreadCounts::FilteredCountInvalidator).to receive(:new).with(account).and_return(invalidator)
expect(notifier).to receive(:perform).ordered.and_return(true)
expect(invalidator).to receive(:conversation_changed!).ordered.and_return(true)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unread",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
end
it 'invalidates filtered unread counts when conversation is marked unread' do
conversation.update!(agent_last_seen_at: 1.minute.from_now, assignee_last_seen_at: 1.minute.from_now)
account.enable_features!(:unread_count_for_filters)
expect do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unread",
headers: agent.create_new_auth_token,
as: :json
end.to change { Conversations::UnreadCounts::FilteredCountStore.conversation_version(account.id) }.by(1)
expect(response).to have_http_status(:success)
end
it 'notifies clients when marking unread only affects filtered counts' do
account.enable_features!(:conversation_unread_counts, :unread_count_for_filters)
conversation.update!(agent_last_seen_at: 1.minute.from_now, assignee_last_seen_at: 1.minute.from_now)
allow(Conversations::UnreadCounts::Refresher).to receive(:new).and_return(
instance_double(Conversations::UnreadCounts::Refresher, perform: false)
)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unread",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch).with(
'conversation.unread_count_changed',
kind_of(Time),
conversation: conversation
)
end
end
end
@@ -70,8 +70,8 @@ RSpec.describe 'DashboardAppsController', type: :request do
end
end
context 'when it is an authenticated user' do
let(:user) { create(:user, account: account) }
context 'when it is an authenticated administrator' do
let(:user) { create(:user, account: account, role: :administrator) }
it 'creates the dashboard app' do
expect do
@@ -130,11 +130,26 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(response).to have_http_status(:unprocessable_entity)
end
end
context 'when it is an authenticated agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'does not create account-wide dashboard apps' do
expect do
post "/api/v1/accounts/#{account.id}/dashboard_apps",
headers: agent.create_new_auth_token,
params: payload,
as: :json
end.not_to change(DashboardApp, :count)
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PATCH /api/v1/accounts/{account.id}/dashboard_apps/:id' do
let(:payload) { { dashboard_app: { title: 'CRM Dashboard', content: [{ type: 'frame', url: 'https://link.com' }] } } }
let(:user) { create(:user, account: account) }
let(:user) { create(:user, account: account, role: :administrator) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
context 'when it is an unauthenticated user' do
@@ -160,10 +175,24 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(json_response['content'][0]['type']).to eq payload[:dashboard_app][:content][0][:type]
end
end
context 'when it is an authenticated agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'does not update account-wide dashboard apps' do
patch "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
headers: agent.create_new_auth_token,
params: payload,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(dashboard_app.reload.title).not_to eq('CRM Dashboard')
end
end
end
describe 'DELETE /api/v1/accounts/{account.id}/dashboard_apps/:id' do
let(:user) { create(:user, account: account) }
let(:user) { create(:user, account: account, role: :administrator) }
let!(:dashboard_app) { create(:dashboard_app, user: user, account: account) }
context 'when it is an unauthenticated user' do
@@ -182,5 +211,18 @@ RSpec.describe 'DashboardAppsController', type: :request do
expect(user.dashboard_apps.count).to be 0
end
end
context 'when it is an authenticated agent' do
let(:agent) { create(:user, account: account, role: :agent) }
it 'does not delete account-wide dashboard apps' do
delete "/api/v1/accounts/#{account.id}/dashboard_apps/#{dashboard_app.id}",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(DashboardApp.exists?(dashboard_app.id)).to be(true)
end
end
end
end
@@ -43,7 +43,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
]
expect(params['scope']).to eq(expected_scope)
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
expect(url).not_to match(/(?:\?|&)prompt=/)
expect(params['prompt']).to eq(['select_account'])
# Validate state parameter exists and can be decoded back to the account
expect(params['state']).to be_present
@@ -116,6 +116,78 @@ RSpec.describe '/api/v1/widget/contacts', type: :request do
end
end
describe 'PATCH /api/v1/widget/contact with HMAC enforcement' do
let(:web_widget) { create(:channel_widget, account: account, hmac_mandatory: true) }
let!(:victim) { create(:contact, account: account, identifier: 'victim-identifier', name: 'Victim') }
let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, 'victim-identifier') }
context 'when an identifier is supplied on a mandatory-hmac inbox' do
it 'rejects when identifier_hash is omitted' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, identifier: 'victim-identifier', name: 'Attacker' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
expect(victim.reload.name).to eq('Victim')
end
it 'rejects when identifier_hash is blank' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, identifier: 'victim-identifier', identifier_hash: '', name: 'Attacker' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
expect(victim.reload.name).to eq('Victim')
end
it 'rejects when identifier_hash is null' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, identifier: 'victim-identifier', identifier_hash: nil, name: 'Attacker' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
expect(victim.reload.name).to eq('Victim')
end
it 'rejects when identifier_hash is invalid' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, identifier: 'victim-identifier',
identifier_hash: 'DEFINITELY_INVALID_AAAAA_NOT_A_REAL_HMAC', name: 'Attacker' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
expect(victim.reload.name).to eq('Victim')
end
it 'succeeds when a valid identifier_hash is provided' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, identifier: 'victim-identifier',
identifier_hash: correct_identifier_hash, name: 'Legit' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
end
end
context 'when no identifier is supplied (anonymous prechat update)' do
it 'allows updating name/email without an identifier_hash' do
patch '/api/v1/widget/contact',
params: { website_token: web_widget.website_token, email: 'prechat@test.com', name: 'Prechat User' },
headers: { 'X-Auth-Token' => token },
as: :json
expect(victim.reload.email).to be_nil
expect(Contact.from_email('prechat@test.com')).to be_present
expect(response).to have_http_status(:success)
end
end
end
describe 'PATCH /api/v1/widget/contact/set_user' do
let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
let(:web_widget) { create(:channel_widget, account: account, hmac_mandatory: true) }
@@ -140,6 +140,51 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
end
it 'saves contact custom attributes on the widget contact' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: {
website_token: web_widget.website_token,
contact: {
name: 'contact-name',
email: 'contact-email@chatwoot.com',
custom_attributes: { cpf: '123.456.789-09' }
},
message: {
content: 'This is a test message'
}
},
as: :json
expect(response).to have_http_status(:success)
expect(contact.reload.custom_attributes['cpf']).to eq('123.456.789-09')
end
it 'saves contact custom attributes on the surviving contact when merged into an existing contact' do
existing_contact = create(:contact, account: account, email: 'contact-email@chatwoot.com', custom_attributes: { 'cpf' => 'old-value' })
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: {
website_token: web_widget.website_token,
contact: {
name: 'contact-name',
email: existing_contact.email,
custom_attributes: { cpf: '123.456.789-09' }
},
message: {
content: 'This is a test message'
}
},
as: :json
expect(response).to have_http_status(:success)
# the widget contact is merged into the existing contact; the freshly
# submitted value must land on the surviving contact and win over stale data
expect(Contact.exists?(contact.id)).to be(false)
expect(existing_contact.reload.custom_attributes['cpf']).to eq('123.456.789-09')
end
it 'doesnt not add phone number if the invalid phone number is provided' do
existing_contact = create(:contact, account: account)
@@ -240,7 +285,8 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: "Conversation was resolved by #{contact.name}"
content: "Conversation was resolved by #{contact.name}",
content_attributes: { activity: { type: 'conversation_status_changed', status: 'resolved' } }
}
)
end
@@ -202,7 +202,8 @@ RSpec.describe '/api/v1/widget/messages', type: :request do
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: "Conversation was resolved by #{contact.name}"
content: "Conversation was resolved by #{contact.name}",
content_attributes: { activity: { type: 'conversation_status_changed', status: 'resolved' } }
}
)
expect(response).to have_http_status(:success)