Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,49 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/campaigns', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let!(:campaign_1) { create(:campaign, inbox: web_widget.inbox, enabled: true, account: account, trigger_rules: { url: 'https://test.com' }) }
let!(:campaign_2) { create(:campaign, inbox: web_widget.inbox, enabled: false, account: account, trigger_rules: { url: 'https://test.com' }) }
describe 'GET /api/v1/widget/campaigns' do
let(:params) { { website_token: web_widget.website_token } }
context 'when campaigns feature is enabled' do
before do
account.enable_features!('campaigns')
end
it 'returns the list of enabled campaigns' do
get '/api/v1/widget/campaigns', params: params
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response.length).to eq 1
expect(json_response.pluck('id')).to include(campaign_1.display_id)
expect(json_response.pluck('id')).not_to include(campaign_2.display_id)
end
end
context 'when campaigns feature is disabled' do
before do
account.disable_features!('campaigns')
end
it 'returns empty array' do
get '/api/v1/widget/campaigns', params: params
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response).to eq []
end
end
context 'with invalid website token' do
it 'returns not found status' do
get '/api/v1/widget/campaigns', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,78 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/config', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let!(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
describe 'POST /api/v1/widget/config' do
let(:params) { { website_token: web_widget.website_token } }
let(:response_keys) { %w[website_channel_config contact global_config] }
context 'with invalid website token' do
it 'returns not found' do
post '/api/v1/widget/config', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token and missing X-Auth-Token' do
it 'returns widget config along with a new contact' do
expect do
post '/api/v1/widget/config',
params: params,
as: :json
end.to change(Contact, :count).by(1)
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.keys).to include(*response_keys)
end
end
context 'with correct website token and valid X-Auth-Token' do
it 'returns widget config along with the same contact' do
expect do
post '/api/v1/widget/config',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
end.not_to change(Contact, :count)
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.keys).to include(*response_keys)
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
end
it 'returns 401 if account is suspended' do
account.update!(status: :suspended)
post '/api/v1/widget/config',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'with correct website token and invalid X-Auth-Token' do
it 'returns widget config and new contact with error message' do
expect do
post '/api/v1/widget/config',
params: params,
headers: { 'X-Auth-Token' => 'invalid token' },
as: :json
end.to change(Contact, :count).by(1)
expect(response).to have_http_status(:success)
response_data = response.parsed_body
expect(response_data.keys).to include(*response_keys)
end
end
end
end

View File

@@ -0,0 +1,209 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/contacts', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:contact) { create(:contact, account: account, email: 'test@test.com', phone_number: '+745623239') }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
describe 'PATCH /api/v1/widget/contact' do
let(:params) { { website_token: web_widget.website_token, identifier: 'test' } }
context 'with invalid website token' do
it 'returns unauthorized' do
patch '/api/v1/widget/contact', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
let(:identify_action) { double }
before do
allow(ContactIdentifyAction).to receive(:new).and_return(identify_action)
allow(identify_action).to receive(:perform).and_return(contact)
end
it 'calls contact identify' do
patch '/api/v1/widget/contact',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
expected_params = { contact: contact, params: params, discard_invalid_attrs: true }
expect(ContactIdentifyAction).to have_received(:new).with(expected_params)
expect(identify_action).to have_received(:perform)
end
end
context 'with update contact' do
let(:params) { { website_token: web_widget.website_token } }
it 'dont update phone number if invalid phone number passed' do
patch '/api/v1/widget/contact',
params: params.merge({ phone_number: '45623239' }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_phone_number']).to be true
contact.reload
expect(contact.phone_number).to eq('+745623239')
expect(response).to have_http_status(:success)
end
it 'update phone number if valid phone number passed' do
patch '/api/v1/widget/contact',
params: params.merge({ phone_number: '+245623239' }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_phone_number']).to be true
contact.reload
expect(contact.phone_number).to eq('+245623239')
expect(response).to have_http_status(:success)
end
it 'dont update email if invalid email passed' do
patch '/api/v1/widget/contact',
params: params.merge({ email: 'test@' }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_email']).to be true
contact.reload
expect(contact.email).to eq('test@test.com')
expect(response).to have_http_status(:success)
end
it 'dont update email if empty value email passed' do
patch '/api/v1/widget/contact',
params: params.merge({ email: '' }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_email']).to be true
contact.reload
expect(contact.email).to eq('test@test.com')
expect(response).to have_http_status(:success)
end
it 'dont update email if nil value email passed' do
patch '/api/v1/widget/contact',
params: params.merge({ email: nil }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_email']).to be true
contact.reload
expect(contact.email).to eq('test@test.com')
expect(response).to have_http_status(:success)
end
it 'update email if valid email passed' do
patch '/api/v1/widget/contact',
params: params.merge({ email: 'test-1@test.com' }),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['has_email']).to be true
contact.reload
expect(contact.email).to eq('test-1@test.com')
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) }
let(:correct_identifier_hash) { OpenSSL::HMAC.hexdigest('sha256', web_widget.hmac_token, params[:identifier].to_s) }
let(:incorrect_identifier_hash) { 'test' }
context 'when the current contact identifier is different from param identifier' do
before do
contact.update(identifier: 'random')
end
it 'return a new contact for the provided identifier' do
patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: correct_identifier_hash),
headers: { 'X-Auth-Token' => token },
as: :json
body = response.parsed_body
expect(body['id']).not_to eq(contact.id)
expect(body['widget_auth_token']).not_to be_nil
expect(Contact.find(body['id']).contact_inboxes.first.hmac_verified?).to be(true)
end
end
context 'with mandatory hmac' do
let(:identify_action) { double }
before do
allow(ContactIdentifyAction).to receive(:new).and_return(identify_action)
allow(identify_action).to receive(:perform).and_return(contact)
end
it 'returns success when correct identifier hash is provided' do
patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: correct_identifier_hash),
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
end
it 'returns error when incorrect identifier hash is provided' do
patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: incorrect_identifier_hash),
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns error when identifier hash is blank' do
patch '/api/v1/widget/contact/set_user',
params: params.merge(identifier_hash: ''),
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'returns error when identifier hash is not provided' do
patch '/api/v1/widget/contact/set_user',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST /api/v1/widget/destroy_custom_attributes' do
let(:params) { { website_token: web_widget.website_token, identifier: 'test', custom_attributes: ['test'] } }
context 'with invalid website token' do
it 'returns unauthorized' do
post '/api/v1/widget/destroy_custom_attributes', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
it 'calls destroy custom attributes' do
post '/api/v1/widget/destroy_custom_attributes',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(contact.reload.custom_attributes).to eq({})
end
end
end
end

View File

@@ -0,0 +1,334 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/conversations/toggle_typing', 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(:second_session) { 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 }
let(:token_without_conversation) do
Widget::TokenService.new(payload: { source_id: second_session.source_id, inbox_id: web_widget.inbox.id }).generate_token
end
def conversation_params
{
website_token: web_widget.website_token,
contact: {
name: 'contact-name',
email: 'contact-email@chatwoot.com',
phone_number: '+919745313456'
},
message: {
content: 'This is a test message'
},
custom_attributes: { order_id: '12345' }
}
end
describe 'GET /api/v1/widget/conversations' do
context 'with a conversation' do
it 'returns the correct conversation params' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
get '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).to eq(conversation.display_id)
expect(json_response['status']).to eq(conversation.status)
end
end
context 'with a conversation but invalid source id' do
it 'returns the correct conversation params' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
payload = { source_id: 'invalid source id', inbox_id: web_widget.inbox.id }
token = Widget::TokenService.new(payload: payload).generate_token
get '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/widget/conversations' do
it 'creates a conversation with correct details' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: conversation_params,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).not_to be_nil
expect(json_response['contact']['email']).to eq 'contact-email@chatwoot.com'
expect(json_response['contact']['phone_number']).to eq '+919745313456'
expect(json_response['contact']['name']).to eq 'contact-name'
end
it 'creates a conversation with correct message and custom attributes' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: conversation_params,
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['custom_attributes']['order_id']).to eq '12345'
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
expect(json_response['messages'][0]['message_type']).to eq 0
end
it 'create a conversation with a name and without an email' do
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: {
website_token: web_widget.website_token,
contact: {
name: 'alphy'
},
message: {
content: 'This is a test message'
}
},
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).not_to be_nil
expect(json_response['contact']['email']).to be_nil
expect(json_response['contact']['name']).to eq 'alphy'
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
end
it 'does not update the name if the contact already exist' do
existing_contact = create(:contact, account: account, email: 'contact-email@chatwoot.com')
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,
phone_number: '+919745313456'
},
message: {
content: 'This is a test message'
},
custom_attributes: { order_id: '12345' }
},
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['id']).not_to be_nil
expect(json_response['contact']['email']).to eq existing_contact.email
expect(json_response['contact']['name']).not_to eq 'contact-name'
expect(json_response['contact']['phone_number']).to eq '+919745313456'
expect(json_response['custom_attributes']['order_id']).to eq '12345'
expect(json_response['messages'][0]['content']).to eq 'This is a test message'
end
it 'doesnt not add phone number if the invalid phone number is provided' do
existing_contact = create(:contact, account: account)
post '/api/v1/widget/conversations',
headers: { 'X-Auth-Token' => token },
params: {
website_token: web_widget.website_token,
contact: {
name: 'contact-name-1',
email: existing_contact.email,
phone_number: '13456'
},
message: {
content: 'This is a test message'
}
},
as: :json
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['contact']['phone_number']).to be_nil
end
end
describe 'POST /api/v1/widget/conversations/toggle_typing' do
context 'with a conversation' do
it 'dispatches the correct typing status' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post '/api/v1/widget/conversations/toggle_typing',
headers: { 'X-Auth-Token' => token },
params: { typing_status: 'on', website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(Conversation::CONVERSATION_TYPING_ON, kind_of(Time), { conversation: conversation, user: contact })
end
end
end
describe 'POST /api/v1/widget/conversations/update_last_seen' do
context 'with a conversation' do
it 'returns the correct conversation params' do
current_time = DateTime.now.utc
allow(DateTime).to receive(:now).and_return(current_time)
allow(Rails.configuration.dispatcher).to receive(:dispatch)
expect(conversation.contact_last_seen_at).to be_nil
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
post '/api/v1/widget/conversations/update_last_seen',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.contact_last_seen_at).not_to be_nil
end
end
end
describe 'POST /api/v1/widget/conversations/transcript' do
context 'with a conversation' do
it 'sends transcript email' do
contact.update(email: 'test@test.com')
mailer = double
allow(ConversationReplyMailer).to receive(:with).and_return(mailer)
allow(mailer).to receive(:conversation_transcript)
post '/api/v1/widget/conversations/transcript',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
expect(mailer).to have_received(:conversation_transcript).with(conversation, 'test@test.com')
contact.update(email: nil)
end
end
end
describe 'GET /api/v1/widget/conversations/toggle_status' do
context 'when user end conversation from widget' do
it 'resolves the conversation' do
expect(conversation.open?).to be true
get '/api/v1/widget/conversations/toggle_status',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.resolved?).to be true
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: "Conversation was resolved by #{contact.name}"
}
)
end
end
context 'when end conversation is not permitted' do
before do
web_widget.end_conversation = false
web_widget.save!
end
it 'returns action not permitted status' do
expect(conversation.open?).to be true
get '/api/v1/widget/conversations/toggle_status',
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:forbidden)
expect(conversation.reload.resolved?).to be false
end
end
context 'when a token without any conversation is used' do
it 'returns not found status' do
get '/api/v1/widget/conversations/toggle_status',
headers: { 'X-Auth-Token' => token_without_conversation },
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:not_found)
end
end
end
describe 'POST /api/v1/widget/conversations/set_custom_attributes' do
let(:params) { { website_token: web_widget.website_token, custom_attributes: { 'product_name': 'Chatwoot' } } }
context 'with invalid website token' do
it 'returns unauthorized' do
post '/api/v1/widget/conversations/set_custom_attributes', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
it 'sets the values when provided' do
post '/api/v1/widget/conversations/set_custom_attributes',
headers: { 'X-Auth-Token' => token },
params: params,
as: :json
expect(response).to have_http_status(:success)
conversation.reload
# conversation custom attributes should have "product_name" key with value "Chatwoot"
expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot')
end
end
end
describe 'POST /api/v1/widget/conversations/destroy_custom_attributes' do
let(:params) { { website_token: web_widget.website_token, custom_attribute: ['product_name'] } }
context 'with invalid website token' do
it 'returns unauthorized' do
post '/api/v1/widget/conversations/destroy_custom_attributes', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
it 'sets the values when provided' do
# ensure conversation has the attribute
conversation.custom_attributes = { 'product_name': 'Chatwoot' }
conversation.save!
expect(conversation.custom_attributes).to include('product_name' => 'Chatwoot')
post '/api/v1/widget/conversations/destroy_custom_attributes',
headers: { 'X-Auth-Token' => token },
params: params,
as: :json
expect(response).to have_http_status(:success)
conversation.reload
# conversation custom attributes should not have "product_name" key with value "Chatwoot"
expect(conversation.custom_attributes).not_to include('product_name' => 'Chatwoot')
end
end
end
end

View File

@@ -0,0 +1,39 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/direct_uploads', 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 }
describe 'POST /api/v1/widget/direct_uploads' do
context 'when post request is made' do
before do
token
contact
payload
end
it 'creates attachment message in conversation' do
post api_v1_widget_direct_uploads_url,
params: {
website_token: web_widget.website_token,
blob: {
filename: 'avatar.png',
byte_size: '1234',
checksum: 'dsjbsdhbfif3874823mnsdbf',
content_type: 'image/png'
}
},
headers: { 'X-Auth-Token' => token }
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['content_type']).to eq('image/png')
end
end
end
end

View File

@@ -0,0 +1,39 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/events', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
let(:token) { Widget::TokenService.new(payload: payload).generate_token }
describe 'POST /api/v1/widget/events' do
let(:params) { { website_token: web_widget.website_token, name: 'webwidget.triggered', event_info: { test_id: 'test' } } }
context 'with invalid website token' do
it 'returns unauthorized' do
post '/api/v1/widget/events', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
context 'with correct website token' do
before do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
end
it 'dispatches the webwidget event' do
post '/api/v1/widget/events',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
expect(Rails.configuration.dispatcher).to have_received(:dispatch)
.with(params[:name], anything, contact_inbox: contact_inbox,
event_info: { test_id: 'test', browser_language: nil, widget_language: nil, browser: anything })
end
end
end
end

View File

@@ -0,0 +1,34 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/inbox_members', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:agent_1) { create(:user, account: account) }
let(:agent_2) { create(:user, account: account) }
before do
create(:inbox_member, user: agent_1, inbox: web_widget.inbox)
create(:inbox_member, user: agent_2, inbox: web_widget.inbox)
end
describe 'GET /api/v1/widget/inbox_members' do
let(:params) { { website_token: web_widget.website_token } }
context 'with correct website token' do
it 'returns the list of agents' do
get '/api/v1/widget/inbox_members', params: params
expect(response).to have_http_status(:success)
json_response = response.parsed_body
expect(json_response['payload'].length).to eq 2
end
end
context 'with invalid website token' do
it 'returns the list of agents' do
get '/api/v1/widget/inbox_members', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,74 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/integrations/dyte', 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 }
let(:message) { create(:message, conversation: conversation, account: account, inbox: conversation.inbox) }
let!(:integration_message) do
create(:message, content_type: 'integrations',
content_attributes: { type: 'dyte', data: { meeting_id: 'm_id' } },
conversation: conversation, account: account, inbox: conversation.inbox)
end
before do
create(:integrations_hook, :dyte, account: account)
end
describe 'POST /api/v1/widget/integrations/dyte/add_participant_to_meeting' do
context 'when token is invalid' do
it 'returns error' do
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
params: { website_token: web_widget.website_token },
as: :json
expect(response).to have_http_status(:not_found)
end
end
context 'when token is valid' do
context 'when message is not an integration message' do
it 'returns error' do
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token, message_id: message.id },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
response_body = response.parsed_body
expect(response_body['error']).to eq('Invalid message type. Action not permitted')
end
end
context 'when message is an integration message' do
before do
stub_request(:post, 'https://api.dyte.io/v2/meetings/m_id/participants')
.to_return(
status: 200,
body: { success: true, data: { id: 'random_uuid', auth_token: 'json-web-token' } }.to_json,
headers: { 'Content-Type' => 'application/json' }
)
end
it 'returns auth_token' do
post add_participant_to_meeting_api_v1_widget_integrations_dyte_url,
headers: { 'X-Auth-Token' => token },
params: { website_token: web_widget.website_token, message_id: integration_message.id },
as: :json
expect(response).to have_http_status(:success)
response_body = response.parsed_body
expect(response_body).to eq(
{
'id' => 'random_uuid', 'auth_token' => 'json-web-token'
}
)
end
end
end
end
end

View File

@@ -0,0 +1,79 @@
require 'rails_helper'
RSpec.describe '/api/v1/widget/labels', type: :request do
let(:account) { create(:account) }
let(:web_widget) { create(:channel_widget, account: account) }
let(:contact) { create(:contact, account: account) }
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 }
describe 'POST /api/v1/widget/labels' do
let(:params) { { website_token: web_widget.website_token, label: 'customer-support' } }
context 'with correct website token and undefined label' do
it 'does not add the label' do
post '/api/v1/widget/labels',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.label_list.count).to eq 0
end
end
context 'with correct website token and a defined label' do
before do
account.labels.create!(title: 'customer-support')
end
it 'add the label to the conversation' do
post '/api/v1/widget/labels',
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.label_list.count).to eq 1
expect(conversation.reload.label_list.first).to eq 'customer-support'
end
end
context 'with invalid website token' do
it 'returns the list of labels' do
post '/api/v1/widget/labels', params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
end
describe 'DELETE /api/v1/widget/labels' do
before do
conversation.label_list.add('customer-support')
conversation.save!
end
let(:params) { { website_token: web_widget.website_token, label: 'customer-support' } }
context 'with correct website token' do
it 'returns the list of labels' do
delete "/api/v1/widget/labels/#{params[:label]}",
params: params,
headers: { 'X-Auth-Token' => token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.label_list.count).to eq 0
end
end
context 'with invalid website token' do
it 'returns the list of labels' do
delete "/api/v1/widget/labels/#{params[:label]}", params: { website_token: '' }
expect(response).to have_http_status(:not_found)
end
end
end
end

View File

@@ -0,0 +1,220 @@
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 '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}"
}
)
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 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