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,51 @@
require 'rails_helper'
RSpec.describe 'Public Inbox Contacts API', type: :request do
let!(:api_channel) { create(:channel_api) }
let!(:contact) { create(:contact) }
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) }
describe 'POST /public/api/v1/inboxes/{identifier}/contact' do
it 'creates a contact and return the source id' do
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.keys).to include('email', 'id', 'name', 'phone_number', 'pubsub_token', 'source_id')
expect(data['source_id']).not_to be_nil
expect(data['pubsub_token']).not_to be_nil
end
it 'persists the identifier of the contact' do
identifier = 'contact-identifier'
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts", params: { identifier: identifier }
expect(response).to have_http_status(:success)
db_contact = api_channel.account.contacts.find_by(identifier: identifier)
expect(db_contact).not_to be_nil
end
end
describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}' do
it 'gets a contact when present' do
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.keys).to include('email', 'id', 'name', 'phone_number', 'pubsub_token', 'source_id')
expect(data['source_id']).to eq contact_inbox.source_id
expect(data['pubsub_token']).to eq contact_inbox.pubsub_token
end
end
describe 'PATCH /public/api/v1/inboxes/{identifier}/contact/{source_id}' do
it 'updates a contact when present' do
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}",
params: { name: 'John Smith' }
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['name']).to eq 'John Smith'
end
end
end

View File

@@ -0,0 +1,143 @@
require 'rails_helper'
RSpec.describe 'Public Inbox Contact Conversations API', type: :request do
let!(:api_channel) { create(:channel_api) }
let!(:contact) { create(:contact) }
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) }
describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations' do
it 'return the conversations for that contact' do
create(:conversation, contact_inbox: contact_inbox)
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.length).to eq 1
expect(data.first['uuid']).to eq contact_inbox.conversations.first.uuid
end
it 'return the conversations when hmac_verified is true' do
contact_inbox.update(hmac_verified: true)
create(:conversation, contact: contact)
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.length).to eq 1
expect(data.first['uuid']).to eq contact.conversations.first.uuid
end
it 'does not return any private or activity message' do
conversation = create(:conversation, contact_inbox: contact_inbox)
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-1')
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-2')
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'private-message-1',
private: true)
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'activity-message-1',
message_type: :activity)
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.length).to eq 1
expect(data.first['messages'].length).to eq 2
expect(data.first['messages'].pluck('content')).not_to include('private-message-1')
expect(data.first['messages'].pluck('message_type')).not_to include('activity')
end
end
describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}' do
it 'returns the conversation that the contact has access to' do
conversation = create(:conversation, contact_inbox: contact_inbox)
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-1')
create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content: 'message-2')
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['id']).to eq(conversation.display_id)
expect(data['messages']).to be_a(Array)
expect(data['messages'].length).to eq(conversation.messages.count)
expect(data['messages'].pluck('content')).to include(conversation.messages.first.content)
end
end
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/toggle_status' do
it 'resolves the conversation' do
conversation = create(:conversation, contact_inbox: contact_inbox)
display_id = conversation.display_id
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{display_id}/toggle_status"
expect(response).to have_http_status(:success)
expect(conversation.reload).to be_resolved
end
it 'does not resolve a conversation that is already resolved' do
conversation = create(:conversation, contact_inbox: contact_inbox, status: :resolved)
display_id = conversation.display_id
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{display_id}/toggle_status"
expect(response).to have_http_status(:success)
expect(Conversation.where(id: conversation.id, status: :resolved).count).to eq(1)
end
end
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations' do
it 'creates a conversation for that contact' do
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['id']).not_to be_nil
end
it 'creates a conversation with custom attributes but prevents other attributes' do
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations",
params: { custom_attributes: { 'test' => 'test' }, additional_attributes: { 'test' => 'test' } }
expect(response).to have_http_status(:success)
data = response.parsed_body
conversation = api_channel.inbox.conversations.find_by(display_id: data['id'])
expect(conversation.custom_attributes).to eq('test' => 'test')
expect(conversation.additional_attributes).to be_empty
end
end
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/toggle_typing' do
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, contact: contact) }
let(:toggle_typing_path) do
"/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/toggle_typing"
end
it 'dispatches the correct typing status' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
post toggle_typing_path, params: { typing_status: 'on' }
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
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/update_last_seen' do
let!(:conversation) { create(:conversation, contact_inbox: contact_inbox, contact: contact) }
let(:update_last_seen_path) do
"/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/update_last_seen"
end
it 'updates the last seen of the conversation contact' do
current_time = DateTime.now.utc
allow(DateTime).to receive(:now).and_return(current_time)
contact_last_seen_at = conversation.contact_last_seen_at
expect(Conversations::UpdateMessageStatusJob).to receive(:perform_later).with(conversation.id, current_time)
post update_last_seen_path
expect(response).to have_http_status(:success)
expect(conversation.reload.contact_last_seen_at).not_to eq contact_last_seen_at
end
end
end

View File

@@ -0,0 +1,18 @@
require 'rails_helper'
RSpec.describe 'Public Inbox API', type: :request do
let!(:api_channel) { create(:channel_api) }
describe 'GET /public/api/v1/inboxes/{identifier}' do
it 'is able to fetch the details of an inbox' do
get "/public/api/v1/inboxes/#{api_channel.identifier}"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.keys).to include('name', 'timezone', 'working_hours', 'working_hours_enabled')
expect(data.keys).to include('csat_survey_enabled', 'greeting_enabled', 'identity_validation_enabled')
expect(data['identifier']).to eq api_channel.identifier
end
end
end

View File

@@ -0,0 +1,99 @@
require 'rails_helper'
RSpec.describe 'Public Inbox Contact Conversation Messages API', type: :request do
let!(:api_channel) { create(:channel_api) }
let!(:contact) { create(:contact, phone_number: '+324234324', email: 'dfsadf@sfsda.com') }
let!(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: api_channel.inbox) }
let!(:conversation) { create(:conversation, contact: contact, contact_inbox: contact_inbox) }
describe 'GET /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages' do
it 'return the messages for that conversation' do
2.times.each { create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation) }
get "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages"
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data.length).to eq 2
end
end
describe 'POST /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages' do
it 'creates a message in the conversation' do
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages",
params: { content: 'hello' }
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['content']).to eq('hello')
end
it 'does not create the message' do
content = "#{'h' * 150 * 1000}a"
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages",
params: { content: content }
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 attachment message in conversation' do
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
post "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/#{conversation.display_id}/messages",
params: { content: 'hello', attachments: [file] }
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['content']).to eq('hello')
expect(conversation.messages.last.attachments.first.file.present?).to be(true)
expect(conversation.messages.last.attachments.first.file_type).to eq('image')
end
end
describe 'PATCH /public/api/v1/inboxes/{identifier}/contact/{source_id}/conversations/{conversation_id}/messages/{id}' do
it 'updates a message in the conversation' do
message = create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation)
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/" \
"#{conversation.display_id}/messages/#{message.id}",
params: { submitted_values: [{ title: 'test' }] }
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['content_attributes']['submitted_values'].first['title']).to eq 'test'
end
it 'updates CSAT survey response for the conversation' do
message = create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content_type: 'input_csat')
# since csat survey is created in async job, we are mocking the creation.
create(:csat_survey_response, conversation: conversation, message: message, rating: 4, feedback_message: 'amazing experience')
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/" \
"#{conversation.display_id}/messages/#{message.id}",
params: { submitted_values: { csat_survey_response: { rating: 4, feedback_message: 'amazing experience' } } },
as: :json
expect(response).to have_http_status(:success)
data = response.parsed_body
expect(data['content_attributes']['submitted_values']['csat_survey_response']['feedback_message']).to eq 'amazing experience'
expect(data['content_attributes']['submitted_values']['csat_survey_response']['rating']).to eq 4
end
it 'returns update error if CSAT message sent more than 14 days' do
message = create(:message, account: conversation.account, inbox: conversation.inbox, conversation: conversation, content_type: 'input_csat',
created_at: 15.days.ago)
# since csat survey is created in async job, we are mocking the creation.
create(:csat_survey_response, conversation: conversation, message: message, rating: 4, feedback_message: 'amazing experience')
patch "/public/api/v1/inboxes/#{api_channel.identifier}/contacts/#{contact_inbox.source_id}/conversations/" \
"#{conversation.display_id}/messages/#{message.id}",
params: { submitted_values: { csat_survey_response: { rating: 4, feedback_message: 'amazing experience' } } },
as: :json
expect(response).to have_http_status(:unprocessable_entity)
end
end
end