Restructure omni services and add Chatwoot research snapshot
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Public Survey Responses API', type: :request do
|
||||
describe 'GET public/api/v1/csat_survey/{uuid}' do
|
||||
it 'return the csat response for that conversation' do
|
||||
conversation = create(:conversation)
|
||||
create(:message, conversation: conversation, content_type: 'input_csat')
|
||||
get "/public/api/v1/csat_survey/#{conversation.uuid}"
|
||||
data = response.parsed_body
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(data['conversation_id']).to eq conversation.id
|
||||
end
|
||||
|
||||
it 'returns not found error for the open conversation' do
|
||||
conversation = create(:conversation)
|
||||
create(:message, conversation: conversation, content_type: 'text')
|
||||
get "/public/api/v1/csat_survey/#{conversation.uuid}"
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT public/api/v1/csat_survey/{uuid}' do
|
||||
params = { message: { submitted_values: { csat_survey_response: { rating: 4, feedback_message: 'amazing experience' } } } }
|
||||
it 'update csat survey response for the conversation' do
|
||||
conversation = create(:conversation)
|
||||
message = create(:message, 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/csat_survey/#{conversation.uuid}",
|
||||
params: params,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:success)
|
||||
data = response.parsed_body
|
||||
expect(data['conversation_id']).to eq conversation.id
|
||||
expect(data['csat_survey_response']['conversation_id']).to eq conversation.id
|
||||
expect(data['csat_survey_response']['feedback_message']).to eq 'amazing experience'
|
||||
expect(data['csat_survey_response']['rating']).to eq 4
|
||||
end
|
||||
|
||||
it 'returns update error if CSAT message sent more than 14 days' do
|
||||
conversation = create(:conversation)
|
||||
message = create(:message, conversation: conversation, content_type: 'input_csat', created_at: 15.days.ago)
|
||||
create(:csat_survey_response, conversation: conversation, message: message, rating: 4, feedback_message: 'amazing experience')
|
||||
patch "/public/api/v1/csat_survey/#{conversation.uuid}",
|
||||
params: params,
|
||||
as: :json
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,147 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Public Articles API', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:portal) { create(:portal, slug: 'test-portal', config: { allowed_locales: %w[en es] }, custom_domain: 'www.example.com') }
|
||||
let!(:category) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'en', slug: 'category_slug') }
|
||||
let!(:category_2) { create(:category, name: 'category', portal: portal, account_id: account.id, locale: 'es', slug: 'category_slug') }
|
||||
let!(:article) do
|
||||
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id,
|
||||
content: 'This is a *test* content with ^markdown^', views: 0)
|
||||
end
|
||||
|
||||
before do
|
||||
ENV['HELPCENTER_URL'] = ENV.fetch('FRONTEND_URL', nil)
|
||||
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, views: 15)
|
||||
create(:article, category: category, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id, views: 1)
|
||||
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, associated_article_id: article.id, views: 5)
|
||||
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, views: 4)
|
||||
create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id, status: :draft)
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/:slug/articles' do
|
||||
it 'Fetch all articles in the portal' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}/articles"
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'Fetches only the published articles in the portal' do
|
||||
get "/hc/#{portal.slug}/#{category_2.locale}/categories/#{category.slug}/articles.json"
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.length).to eq(2)
|
||||
end
|
||||
|
||||
it 'get all articles with searched text query' do
|
||||
article2 = create(:article,
|
||||
account_id: account.id,
|
||||
portal: portal,
|
||||
category: category,
|
||||
author_id: agent.id,
|
||||
content: 'this is some test and funny content')
|
||||
expect(article2.id).not_to be_nil
|
||||
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}/articles.json", params: { query: 'funny' }
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.length).to eq(1)
|
||||
end
|
||||
|
||||
it 'get all popular articles if sort params is passed' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/articles.json", params: { sort: 'views' }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.length).to eq(3)
|
||||
expect(response_data[0][:views]).to eq(15)
|
||||
expect(response_data[1][:views]).to eq(1)
|
||||
expect(response_data.last[:id]).to eq(article.id)
|
||||
end
|
||||
|
||||
it 'limits results based on per_page parameter' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/articles.json", params: { per_page: 2 }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.length).to eq(2)
|
||||
# Only count articles in the current locale (category.locale is 'en')
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:articles_count]).to eq(3)
|
||||
end
|
||||
|
||||
it 'returns articles count from all locales when locale parameter is not present' do
|
||||
get "/hc/#{portal.slug}/articles.json"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(JSON.parse(response.body, symbolize_names: true)[:meta][:articles_count]).to eq(5)
|
||||
end
|
||||
|
||||
it 'uses default items per page if per_page is less than 1' do
|
||||
get "/hc/#{portal.slug}/#{category.locale}/articles.json", params: { per_page: 0 }
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
response_data = JSON.parse(response.body, symbolize_names: true)[:payload]
|
||||
expect(response_data.length).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/:slug/articles/:id' do
|
||||
it 'Fetch article with the id' do
|
||||
get "/hc/#{portal.slug}/articles/#{article.slug}"
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(ChatwootMarkdownRenderer.new(article.content).render_article)
|
||||
expect(article.reload.views).to eq 0 # View count should not increment on show
|
||||
end
|
||||
|
||||
it 'does not increment the view count if the article is not published' do
|
||||
draft_article = create(:article, category: category, status: :draft, portal: portal, account_id: account.id, author_id: agent.id, views: 0)
|
||||
get "/hc/#{portal.slug}/articles/#{draft_article.slug}"
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(draft_article.reload.views).to eq 0
|
||||
end
|
||||
|
||||
it 'returns the article with the id with a different locale' do
|
||||
article_in_locale = create(:article, category: category_2, portal: portal, account_id: account.id, author_id: agent.id)
|
||||
get "/hc/#{portal.slug}/articles/#{article_in_locale.slug}"
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/:slug/articles/:slug.png (tracking pixel)' do
|
||||
it 'serves a PNG image and increments view count for published article' do
|
||||
get "/hc/#{portal.slug}/articles/#{article.slug}.png"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.headers['Content-Type']).to eq('image/png')
|
||||
expect(response.headers['Cache-Control']).to include('max-age=86400')
|
||||
expect(response.headers['Cache-Control']).to include('private')
|
||||
expect(article.reload.views).to eq 1
|
||||
end
|
||||
|
||||
it 'serves a PNG image but does not increment view count for draft article' do
|
||||
draft_article = create(:article, category: category, status: :draft, portal: portal, account_id: account.id, author_id: agent.id, views: 0)
|
||||
|
||||
get "/hc/#{portal.slug}/articles/#{draft_article.slug}.png"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.headers['Content-Type']).to eq('image/png')
|
||||
expect(response.headers['Cache-Control']).to include('max-age=86400')
|
||||
expect(response.headers['Cache-Control']).to include('private')
|
||||
expect(draft_article.reload.views).to eq 0
|
||||
end
|
||||
|
||||
it 'returns 404 if article does not exist' do
|
||||
get "/hc/#{portal.slug}/articles/non-existent-article.png"
|
||||
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
|
||||
it 'sets proper cache headers for performance' do
|
||||
get "/hc/#{portal.slug}/articles/#{article.slug}.png"
|
||||
|
||||
expect(response.headers['Cache-Control']).to include('max-age=86400')
|
||||
expect(response.headers['Cache-Control']).to include('private')
|
||||
expect(response.headers['Content-Type']).to eq('image/png')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,32 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Public Categories API', type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:portal) { create(:portal, slug: 'test-portal', custom_domain: 'www.example.com') }
|
||||
|
||||
before do
|
||||
create(:category, slug: 'test-category-1', portal_id: portal.id, account_id: account.id)
|
||||
create(:category, slug: 'test-category-2', portal_id: portal.id, account_id: account.id)
|
||||
create(:category, slug: 'test-category-3', portal_id: portal.id, account_id: account.id)
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/:portal_slug/categories' do
|
||||
it 'Fetch all categories in the portal' do
|
||||
category = portal.categories.first
|
||||
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/:portal_slug/categories/:slug' do
|
||||
it 'Fetch category with the slug' do
|
||||
category = portal.categories.first
|
||||
|
||||
get "/hc/#{portal.slug}/#{category.locale}/categories/#{category.slug}"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,95 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Public::Api::V1::PortalsController, type: :request do
|
||||
let!(:account) { create(:account) }
|
||||
let!(:agent) { create(:user, account: account, role: :agent) }
|
||||
let!(:portal) { create(:portal, slug: 'test-portal', account_id: account.id, custom_domain: 'www.example.com') }
|
||||
|
||||
before do
|
||||
create(:portal, slug: 'test-portal-1', account_id: account.id)
|
||||
create(:portal, slug: 'test-portal-2', account_id: account.id)
|
||||
create_list(:article, 3, account: account, author: agent, portal: portal, status: :published)
|
||||
create_list(:article, 2, account: account, author: agent, portal: portal, status: :draft)
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/{portal_slug}' do
|
||||
it 'Show portal and categories belonging to the portal' do
|
||||
get "/hc/#{portal.slug}/en"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'Throws unauthorised error for unknown domain' do
|
||||
portal.update(custom_domain: 'www.something.com')
|
||||
|
||||
get "/hc/#{portal.slug}/en"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
json_response = response.parsed_body
|
||||
|
||||
expect(json_response['error']).to eql "Domain: www.example.com is not registered with us. \
|
||||
Please send us an email at support@chatwoot.com with the custom domain name and account API key"
|
||||
end
|
||||
|
||||
context 'when portal has a logo' do
|
||||
it 'includes the logo as favicon' do
|
||||
# Attach a test image to the portal
|
||||
file = Rails.root.join('spec/assets/sample.png').open
|
||||
portal.logo.attach(io: file, filename: 'sample.png', content_type: 'image/png')
|
||||
file.close
|
||||
|
||||
get "/hc/#{portal.slug}/en"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include('<link rel="icon" href=')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when portal has no logo' do
|
||||
it 'does not include a favicon link' do
|
||||
# Ensure logo is not attached
|
||||
portal.logo.purge if portal.logo.attached?
|
||||
|
||||
get "/hc/#{portal.slug}/en"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).not_to include('<link rel="icon" href=')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /public/api/v1/portals/{portal_slug}/sitemap' do
|
||||
context 'when custom_domain is present' do
|
||||
it 'returns a valid urlset sitemap with the correct namespace' do
|
||||
get "/hc/#{portal.slug}/sitemap.xml"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
doc = Nokogiri::XML(response.body)
|
||||
expect(doc.errors).to be_empty
|
||||
|
||||
expect(doc.root.name).to eq('urlset')
|
||||
expect(doc.root.namespace&.href).to eq('http://www.sitemaps.org/schemas/sitemap/0.9')
|
||||
end
|
||||
|
||||
it 'contains valid article URLs for the portal' do
|
||||
get "/hc/#{portal.slug}/sitemap.xml"
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
doc = Nokogiri::XML(response.body)
|
||||
doc.remove_namespaces!
|
||||
|
||||
# ensure we are NOT returning a sitemapindex
|
||||
expect(doc.xpath('//sitemapindex')).to be_empty
|
||||
|
||||
links = doc.xpath('//url/loc').map(&:text)
|
||||
expect(links.length).to eq(3)
|
||||
|
||||
expect(links).to all(
|
||||
match(%r{\Ahttps://www\.example\.com/hc/#{Regexp.escape(portal.slug)}/articles/\d+})
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user