feat: Add native support for CSML in agent_bot API (#4913)

This commit is contained in:
Pranav Raj S
2022-06-23 19:17:46 +05:30
committed by GitHub
parent f71980bd95
commit b7606e4dd2
26 changed files with 722 additions and 80 deletions

View File

@@ -3,5 +3,11 @@ FactoryBot.define do
name { 'MyString' }
description { 'MyString' }
outgoing_url { 'MyString' }
bot_config { {} }
bot_type { 'webhook' }
trait :skip_validate do
to_create { |instance| instance.save(validate: false) }
end
end
end

View File

@@ -0,0 +1,19 @@
require 'rails_helper'
RSpec.describe AgentBots::CsmlJob, type: :job do
it 'runs csml processor service' do
event = 'message.created'
message = create(:message)
agent_bot = create(:agent_bot)
processor = double
allow(Integrations::Csml::ProcessorService).to receive(:new).and_return(processor)
allow(processor).to receive(:perform)
described_class.perform_now(event, agent_bot, message)
expect(Integrations::Csml::ProcessorService)
.to have_received(:new)
.with(event_name: event, agent_bot: agent_bot, event_data: { message: message })
end
end

View File

@@ -1,6 +1,8 @@
require 'rails_helper'
RSpec.describe AgentBotJob, type: :job do
RSpec.describe AgentBots::WebhookJob, type: :job do
include ActiveJob::TestHelper
subject(:job) { described_class.perform_later(url, payload) }
let(:url) { 'https://test.com' }
@@ -11,4 +13,9 @@ RSpec.describe AgentBotJob, type: :job do
.with(url, payload)
.on_queue('bots')
end
it 'executes perform' do
expect(Webhooks::Trigger).to receive(:execute).with(url, payload)
perform_enqueued_jobs { job }
end
end

View File

@@ -1,9 +1,11 @@
require 'rails_helper'
RSpec.describe WebhookJob, type: :job do
include ActiveJob::TestHelper
subject(:job) { described_class.perform_later(url, payload) }
let(:url) { 'https://test.com' }
let(:url) { 'https://test.chatwoot.com' }
let(:payload) { { name: 'test' } }
it 'queues the job' do
@@ -11,4 +13,9 @@ RSpec.describe WebhookJob, type: :job do
.with(url, payload)
.on_queue('webhooks')
end
it 'executes perform' do
expect(Webhooks::Trigger).to receive(:execute).with(url, payload)
perform_enqueued_jobs { job }
end
end

View File

@@ -0,0 +1,99 @@
require 'rails_helper'
describe CsmlEngine do
it 'raises an exception if host and api is absent' do
expect { described_class.new }.to raise_error(StandardError)
end
context 'when CSML_BOT_HOST & CSML_BOT_API_KEY is present' do
before do
create(:installation_config, { name: 'CSML_BOT_HOST', value: 'https://csml.chatwoot.dev' })
create(:installation_config, { name: 'CSML_BOT_API_KEY', value: 'random_api_key' })
end
let(:csml_request) { double }
context 'when status is called' do
it 'returns api response if client response is valid' do
allow(HTTParty).to receive(:get).and_return(csml_request)
allow(csml_request).to receive(:success?).and_return(true)
allow(csml_request).to receive(:parsed_response).and_return({ 'engine_version': '1.11.1' })
response = described_class.new.status
expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status')
expect(csml_request).to have_received(:success?)
expect(csml_request).to have_received(:parsed_response)
expect(response).to eq({ 'engine_version': '1.11.1' })
end
it 'returns error if client response is invalid' do
allow(HTTParty).to receive(:get).and_return(csml_request)
allow(csml_request).to receive(:success?).and_return(false)
allow(csml_request).to receive(:code).and_return(401)
allow(csml_request).to receive(:parsed_response).and_return({ 'error': true })
response = described_class.new.status
expect(HTTParty).to have_received(:get).with('https://csml.chatwoot.dev/status')
expect(csml_request).to have_received(:success?)
expect(response).to eq({ error: { 'error': true }, status: 401 })
end
end
context 'when run is called' do
it 'returns api response if client response is valid' do
allow(HTTParty).to receive(:post).and_return(csml_request)
allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc')
allow(csml_request).to receive(:success?).and_return(true)
allow(csml_request).to receive(:parsed_response).and_return({ 'success': true })
response = described_class.new.run({ flow: 'default' }, { client: 'client', payload: { id: 1 }, metadata: {} })
payload = {
bot: { flow: 'default' },
event: {
request_id: 'xxxx-yyyy-wwww-cccc',
client: 'client',
payload: { id: 1 },
metadata: {},
ttl_duration: 4000
}
}
expect(HTTParty).to have_received(:post)
.with(
'https://csml.chatwoot.dev/run', {
body: payload.to_json,
headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' }
}
)
expect(csml_request).to have_received(:success?)
expect(csml_request).to have_received(:parsed_response)
expect(response).to eq({ 'success': true })
end
end
context 'when validate is called' do
it 'returns api response if client response is valid' do
allow(HTTParty).to receive(:post).and_return(csml_request)
allow(SecureRandom).to receive(:uuid).and_return('xxxx-yyyy-wwww-cccc')
allow(csml_request).to receive(:success?).and_return(true)
allow(csml_request).to receive(:parsed_response).and_return({ 'success': true })
payload = { flow: 'default' }
response = described_class.new.validate(payload)
expect(HTTParty).to have_received(:post)
.with(
'https://csml.chatwoot.dev/validate', {
body: payload.to_json,
headers: { 'X-Api-Key' => 'random_api_key', 'Content-Type' => 'application/json' }
}
)
expect(csml_request).to have_received(:success?)
expect(csml_request).to have_received(:parsed_response)
expect(response).to eq({ 'success': true })
end
end
end
end

View File

@@ -0,0 +1,108 @@
require 'rails_helper'
describe Integrations::Csml::ProcessorService do
let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) }
let(:agent_bot) { create(:agent_bot, :skip_validate, bot_type: 'csml', account: account) }
let(:agent_bot_inbox) { create(:agent_bot_inbox, agent_bot: agent_bot, inbox: inbox, account: account) }
let(:conversation) { create(:conversation, account: account, status: :pending) }
let(:message) { create(:message, account: account, conversation: conversation) }
let(:event_name) { 'message.created' }
let(:event_data) { { message: message } }
describe '#perform' do
let(:csml_client) { double }
let(:processor) { described_class.new(event_name: event_name, agent_bot: agent_bot, event_data: event_data) }
before do
allow(CsmlEngine).to receive(:new).and_return(csml_client)
end
context 'when a conversation is completed from CSML' do
it 'open the conversation and handsoff it to an agent' do
csml_response = ActiveSupport::HashWithIndifferentAccess.new(conversation_end: true)
allow(csml_client).to receive(:run).and_return(csml_response)
processor.perform
expect(conversation.reload.status).to eql('open')
end
end
context 'when a new message is returned from CSML' do
it 'creates a text message' do
csml_response = ActiveSupport::HashWithIndifferentAccess.new(
messages: [
{ payload: { content_type: 'text', content: { text: 'hello payload' } } }
]
)
allow(csml_client).to receive(:run).and_return(csml_response)
processor.perform
expect(conversation.messages.last.content).to eql('hello payload')
end
it 'creates a question message' do
csml_response = ActiveSupport::HashWithIndifferentAccess.new(
messages: [{
payload: {
content_type: 'question',
content: { title: 'Question Payload', buttons: [{ content: { title: 'Q1', payload: 'q1' } }] }
}
}]
)
allow(csml_client).to receive(:run).and_return(csml_response)
processor.perform
expect(conversation.messages.last.content).to eql('Question Payload')
expect(conversation.messages.last.content_type).to eql('input_select')
expect(conversation.messages.last.content_attributes).to eql({ items: [{ title: 'Q1', value: 'q1' }] }.with_indifferent_access)
end
end
context 'when conversation status is not pending' do
let(:conversation) { create(:conversation, account: account, status: :open) }
it 'returns nil' do
expect(processor.perform).to be(nil)
end
end
context 'when message is private' do
let(:message) { create(:message, account: account, conversation: conversation, private: true) }
it 'returns nil' do
expect(processor.perform).to be(nil)
end
end
context 'when message type is template (not outgoing or incoming)' do
let(:message) { create(:message, account: account, conversation: conversation, message_type: :template) }
it 'returns nil' do
expect(processor.perform).to be(nil)
end
end
context 'when message updated' do
let(:event_name) { 'message.updated' }
context 'when content_type is input_select' do
let(:message) do
create(:message, account: account, conversation: conversation, private: true,
submitted_values: [{ 'title' => 'Support', 'value' => 'selected_gas' }])
end
it 'returns submitted value for message content' do
expect(processor.send(:message_content, message)).to eql('selected_gas')
end
end
context 'when content_type is not input_select' do
let(:message) { create(:message, account: account, conversation: conversation, message_type: :outgoing, content_type: :text) }
let(:event_name) { 'message.updated' }
it 'returns nil' do
expect(processor.perform).to be(nil)
end
end
end
end
end

View File

@@ -24,7 +24,7 @@ describe Integrations::Dialogflow::ProcessorService do
before do
allow(dialogflow_service).to receive(:query_result).and_return(dialogflow_response)
allow(processor).to receive(:get_dialogflow_response).and_return(dialogflow_service)
allow(processor).to receive(:get_response).and_return(dialogflow_service)
allow(dialogflow_text_double).to receive(:to_h).and_return({ text: ['hello payload'] })
end

View File

@@ -6,18 +6,18 @@ describe AgentBotListener do
let!(:inbox) { create(:inbox, account: account) }
let!(:agent_bot) { create(:agent_bot) }
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user) }
let!(:message) do
create(:message, message_type: 'outgoing',
account: account, inbox: inbox, conversation: conversation)
end
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
describe '#message_created' do
let(:event_name) { :'conversation.created' }
let(:event_name) { 'message.created' }
let!(:event) { Events::Base.new(event_name, Time.zone.now, message: message) }
let!(:message) do
create(:message, message_type: 'outgoing',
account: account, inbox: inbox, conversation: conversation)
end
context 'when agent bot is not configured' do
it 'does not send message to agent bot' do
expect(AgentBotJob).to receive(:perform_later).exactly(0).times
expect(AgentBots::WebhookJob).to receive(:perform_later).exactly(0).times
listener.message_created(event)
end
end
@@ -25,9 +25,52 @@ describe AgentBotListener do
context 'when agent bot is configured' do
it 'sends message to agent bot' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
expect(AgentBotJob).to receive(:perform_later).with(agent_bot.outgoing_url, message.webhook_data.merge(event: 'message_created')).once
expect(AgentBots::WebhookJob).to receive(:perform_later).with(agent_bot.outgoing_url,
message.webhook_data.merge(event: 'message_created')).once
listener.message_created(event)
end
it 'does not send message to agent bot if url is empty' do
agent_bot = create(:agent_bot, outgoing_url: '')
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
expect(AgentBots::WebhookJob).not_to receive(:perform_later)
listener.message_created(event)
end
end
context 'when agent bot csml type is configured' do
it 'sends message to agent bot' do
agent_bot_csml = create(:agent_bot, :skip_validate, bot_type: 'csml')
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot_csml)
expect(AgentBots::CsmlJob).to receive(:perform_later).with('message.created', agent_bot_csml, message).once
listener.message_created(event)
end
end
end
describe '#webwidget_triggered' do
let(:event_name) { 'webwidget.triggered' }
context 'when agent bot is configured' do
it 'send message to agent bot URL' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
event = double
allow(event).to receive(:data)
.and_return(
{
contact_inbox: conversation.contact_inbox,
event_info: { country: 'US' }
}
)
expect(AgentBots::WebhookJob).to receive(:perform_later)
.with(
agent_bot.outgoing_url,
conversation.contact_inbox.webhook_data.merge(event: 'webwidget_triggered', event_info: { country: 'US' })
).once
listener.webwidget_triggered(event)
end
end
end
end

View File

@@ -0,0 +1,25 @@
require 'rails_helper'
describe AgentBots::ValidateBotService do
describe '#perform' do
it 'returns true if bot_type is not csml' do
agent_bot = create(:agent_bot)
valid = described_class.new(agent_bot: agent_bot).perform
expect(valid).to be true
end
it 'returns true if validate csml returns true' do
agent_bot = create(:agent_bot, :skip_validate, bot_type: 'csml', bot_config: {})
csml_client = double
csml_response = double
allow(CsmlEngine).to receive(:new).and_return(csml_client)
allow(csml_client).to receive(:validate).and_return(csml_response)
allow(csml_response).to receive(:blank?).and_return(false)
allow(csml_response).to receive(:[]).with('valid').and_return(true)
valid = described_class.new(agent_bot: agent_bot).perform
expect(valid).to be true
expect(CsmlEngine).to have_received(:new)
end
end
end