Adds Skooma-based OpenAPI validation so SDK-facing request specs can assert that documented request and response contracts match real Rails behavior. This also upgrades the spec to OpenAPI 3.1 and fixes contract drift uncovered while validating core application and platform resources. Closes None Why We want CI to catch OpenAPI drift before it reaches SDK consumers. While wiring validation in, this PR surfaced several mismatches between the documented contract and what the Rails endpoints actually accept or return. What this change does - Adds Skooma-backed OpenAPI validation to the request spec flow and a dedicated OpenAPI validation spec. - Migrates nullable schema definitions to OpenAPI 3.1-compatible unions. - Updates core SDK-facing schemas and payloads across accounts, contacts, conversations, inboxes, messages, teams, reporting events, and platform account resources. - Documents concrete runtime cases that were previously missing or inaccurate, including nested `profile` update payloads, multipart avatar uploads, required profile update bodies, nullable inbox feature flags, and message sender types that include both `Captain::Assistant` and senderless activity-style messages. - Regenerates the committed Swagger JSON and tag-group artifacts used by CI sync checks. Validation - `bundle exec rake swagger:build` - `bundle exec rspec spec/swagger/openapi_spec.rb` --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
821 lines
33 KiB
Ruby
821 lines
33 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe 'Contacts API', type: :request do
|
|
let(:account) { create(:account) }
|
|
let(:email_filter) do
|
|
{
|
|
attribute_key: 'email',
|
|
filter_operator: 'contains',
|
|
values: 'looped',
|
|
query_operator: 'and',
|
|
attribute_model: 'standard',
|
|
custom_attribute_type: ''
|
|
}
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let!(:contact) { create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Company 1', country_code: 'IN' }) }
|
|
let!(:contact_1) do
|
|
create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Test Company 1', country_code: 'CA' })
|
|
end
|
|
let(:contact_2) do
|
|
create(:contact, :with_email, account: account, additional_attributes: { company_name: 'Marvel Company', country_code: 'AL' })
|
|
end
|
|
let(:contact_3) do
|
|
create(:contact, :with_email, account: account, additional_attributes: { company_name: nil, country_code: nil })
|
|
end
|
|
let!(:contact_4) do
|
|
create(:contact, :with_email, account: account, additional_attributes: { company_name: nil, country_code: nil })
|
|
end
|
|
let!(:contact_inbox) { create(:contact_inbox, contact: contact) }
|
|
|
|
it 'returns all resolved contacts along with contact inboxes' do
|
|
get "/api/v1/accounts/#{account.id}/contacts",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
response_body = response.parsed_body
|
|
contact_emails = response_body['payload'].pluck('email')
|
|
contact_inboxes_source_ids = response_body['payload'].flat_map { |c| c['contact_inboxes'].pluck('source_id') }
|
|
|
|
expect(contact_emails).to include(contact.email)
|
|
expect(contact_inboxes_source_ids).to include(contact_inbox.source_id)
|
|
end
|
|
|
|
it 'returns all contacts without contact inboxes' do
|
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
|
|
contact_emails = response_body['payload'].pluck('email')
|
|
contact_inboxes = response_body['payload'].pluck('contact_inboxes').flatten.compact
|
|
expect(contact_emails).to include(contact.email)
|
|
expect(contact_inboxes).to eq([])
|
|
end
|
|
|
|
it 'returns limited information on inboxes' do
|
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=true",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
|
|
contact_emails = response_body['payload'].pluck('email')
|
|
contact_inboxes = response_body['payload'].pluck('contact_inboxes').flatten.compact
|
|
expect(contact_emails).to include(contact.email)
|
|
first_inbox = contact_inboxes[0]['inbox']
|
|
expect(first_inbox).to be_a(Hash)
|
|
expect(first_inbox).to include('id', 'channel_id', 'channel_type', 'name', 'avatar_url', 'provider')
|
|
|
|
expect(first_inbox).not_to include('imap_login',
|
|
'imap_password',
|
|
'imap_address',
|
|
'imap_port',
|
|
'imap_enabled',
|
|
'imap_enable_ssl')
|
|
|
|
expect(first_inbox).not_to include('smtp_login',
|
|
'smtp_password',
|
|
'smtp_address',
|
|
'smtp_port',
|
|
'smtp_enabled',
|
|
'smtp_domain')
|
|
|
|
expect(first_inbox).not_to include('hmac_token', 'provider_config')
|
|
end
|
|
|
|
it 'returns all contacts with company name desc order' do
|
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=-company",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['payload'].last['id']).to eq(contact_4.id)
|
|
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
|
end
|
|
|
|
it 'returns all contacts with company name asc order with null values at last' do
|
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=-company",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['payload'].first['email']).to eq(contact_1.email)
|
|
expect(response_body['payload'].first['id']).to eq(contact_1.id)
|
|
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
|
end
|
|
|
|
it 'returns all contacts with country name desc order with null values at last' do
|
|
contact_from_albania = create(:contact, :with_email, account: account, additional_attributes: { country_code: 'AL', country: 'Albania' })
|
|
get "/api/v1/accounts/#{account.id}/contacts?include_contact_inboxes=false&sort=country",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['payload'].first['email']).to eq(contact_from_albania.email)
|
|
expect(response_body['payload'].first['id']).to eq(contact_from_albania.id)
|
|
expect(response_body['payload'].last['email']).to eq(contact_4.email)
|
|
end
|
|
|
|
it 'returns last seen at' do
|
|
create(:conversation, contact: contact, account: account, inbox: contact_inbox.inbox, contact_last_seen_at: Time.now.utc)
|
|
get "/api/v1/accounts/#{account.id}/contacts",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['payload'].first['last_seen_at']).present?
|
|
end
|
|
|
|
it 'filters resolved contacts based on label filter' do
|
|
contact_with_label1, contact_with_label2 = FactoryBot.create_list(:contact, 2, :with_email, account: account)
|
|
contact_with_label1.update_labels(['label1'])
|
|
contact_with_label2.update_labels(['label2'])
|
|
|
|
get "/api/v1/accounts/#{account.id}/contacts",
|
|
params: { labels: %w[label1 label2] },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['meta']['count']).to eq(2)
|
|
expect(response_body['payload'].pluck('email')).to include(contact_with_label1.email, contact_with_label2.email)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/contacts/import' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/import"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with out permission' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/import",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'creates a data import' do
|
|
file = fixture_file_upload(Rails.root.join('spec/assets/contacts.csv'), 'text/csv')
|
|
post "/api/v1/accounts/#{account.id}/contacts/import",
|
|
headers: admin.create_new_auth_token,
|
|
params: { import_file: file }
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(account.data_imports.count).to eq(1)
|
|
expect(account.data_imports.first.import_file.attached?).to be(true)
|
|
end
|
|
end
|
|
|
|
context 'when file is empty' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'returns Unprocessable Entity' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/import",
|
|
headers: admin.create_new_auth_token
|
|
|
|
json_response = response.parsed_body
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(json_response['error']).to eq('File is blank')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/contacts/export' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/export"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user with out permission' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/export",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'enqueues a contact export job' do
|
|
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, nil, { :payload => nil, :label => nil }).once
|
|
|
|
post "/api/v1/accounts/#{account.id}/contacts/export",
|
|
headers: admin.create_new_auth_token
|
|
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
|
|
it 'enqueues a contact export job with sent_columns' do
|
|
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, %w[phone_number email],
|
|
{ :payload => nil, :label => nil }).once
|
|
|
|
post "/api/v1/accounts/#{account.id}/contacts/export",
|
|
headers: admin.create_new_auth_token,
|
|
params: { column_names: %w[phone_number email] }
|
|
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
|
|
it 'enqueues a contact export job with payload' do
|
|
expect(Account::ContactsExportJob).to receive(:perform_later).with(account.id, admin.id, nil,
|
|
{
|
|
:payload => [ActionController::Parameters.new(email_filter).permit!],
|
|
:label => nil
|
|
}).once
|
|
|
|
post "/api/v1/accounts/#{account.id}/contacts/export",
|
|
headers: admin.create_new_auth_token,
|
|
params: { payload: [email_filter] }
|
|
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts/active' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/active"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let!(:contact) { create(:contact, account: account) }
|
|
|
|
it 'returns no contacts if no are online' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/active",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.body).not_to include(contact.name)
|
|
end
|
|
|
|
it 'returns all contacts who are online' do
|
|
allow(OnlineStatusTracker).to receive(:get_available_contact_ids).and_return([contact.id])
|
|
|
|
get "/api/v1/accounts/#{account.id}/contacts/active",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.body).to include(contact.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts/search' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/search"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let!(:contact1) { create(:contact, :with_email, account: account) }
|
|
let!(:contact2) { create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com') }
|
|
|
|
it 'returns all resolved contacts with contact inboxes' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: contact2.email },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(response.body).to include(contact2.email)
|
|
expect(response.body).not_to include(contact1.email)
|
|
end
|
|
|
|
it 'matches the contact ignoring the case in email' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: 'Test@Test.com' },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.body).to include(contact2.email)
|
|
expect(response.body).not_to include(contact1.email)
|
|
end
|
|
|
|
it 'matches the contact ignoring the case in name' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: 'TestContact' },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.body).to include(contact2.email)
|
|
expect(response.body).not_to include(contact1.email)
|
|
end
|
|
|
|
it 'matches the resolved contact respecting the identifier character casing' do
|
|
contact_normal = create(:contact, name: 'testcontact', account: account, identifier: 'testidentifer')
|
|
contact_special = create(:contact, name: 'testcontact', account: account, identifier: 'TestIdentifier')
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: 'TestIdentifier' },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.body).to include(contact_special.identifier)
|
|
expect(response.body).not_to include(contact_normal.identifier)
|
|
end
|
|
|
|
it 'returns has_more as false when results fit in one page' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: contact2.email },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['meta']['has_more']).to be(false)
|
|
expect(response_body['meta']['count']).to eq(1)
|
|
end
|
|
|
|
it 'returns has_more as true when there are more results' do
|
|
# Create 16 contacts (more than RESULTS_PER_PAGE which is 15)
|
|
create_list(:contact, 16, account: account, name: 'searchable_contact')
|
|
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: 'searchable_contact' },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['meta']['has_more']).to be(true)
|
|
expect(response_body['meta']['count']).to eq(15)
|
|
expect(response_body['payload'].length).to eq(15)
|
|
end
|
|
|
|
it 'returns has_more as false on the last page' do
|
|
# Create 16 contacts
|
|
create_list(:contact, 16, account: account, name: 'searchable_contact')
|
|
|
|
get "/api/v1/accounts/#{account.id}/contacts/search",
|
|
params: { q: 'searchable_contact', page: 2 },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
response_body = response.parsed_body
|
|
expect(response_body['meta']['has_more']).to be(false)
|
|
expect(response_body['meta']['count']).to eq(1)
|
|
expect(response_body['payload'].length).to eq(1)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts/filter' do
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/filter"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let!(:contact1) { create(:contact, :with_email, account: account, additional_attributes: { country_code: 'US' }) }
|
|
let!(:contact2) do
|
|
create(:contact, :with_email, name: 'testcontact', account: account, email: 'test@test.com', additional_attributes: { country_code: 'US' })
|
|
end
|
|
|
|
it 'returns all contacts when query is empty' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/filter",
|
|
params: { payload: [
|
|
attribute_key: 'country_code',
|
|
filter_operator: 'equal_to',
|
|
values: ['US']
|
|
] },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(response.body).to include(contact2.email)
|
|
expect(response.body).to include(contact1.email)
|
|
end
|
|
|
|
it 'returns error the query operator is invalid' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/filter",
|
|
params: { payload: [
|
|
attribute_key: 'country_code',
|
|
filter_operator: 'eq',
|
|
values: ['US']
|
|
] },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.body).to include('Invalid operator. The allowed operators for country_code are [equal_to,not_equal_to]')
|
|
end
|
|
|
|
it 'returns error the query value is invalid' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/filter",
|
|
params: { payload: [
|
|
attribute_key: 'country_code',
|
|
filter_operator: 'equal_to',
|
|
values: []
|
|
] },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.body).to include('Invalid value. The values provided for country_code are invalid"')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts/:id' do
|
|
let!(:contact) { create(:contact, account: account) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'shows the contact' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(response.body).to include(contact.name)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/contacts/:id/contactable_inboxes' do
|
|
let!(:contact) { create(:contact, account: account) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
|
|
let!(:twilio_sms_inbox) { create(:inbox, channel: twilio_sms, account: account) }
|
|
let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
|
|
let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
|
|
|
|
it 'shows the contactable inboxes which the user has access to' do
|
|
create(:inbox_member, user: agent, inbox: twilio_whatsapp_inbox)
|
|
|
|
inbox_service = double
|
|
allow(Contacts::ContactableInboxesService).to receive(:new).and_return(inbox_service)
|
|
allow(inbox_service).to receive(:get).and_return([
|
|
{ source_id: '1123', inbox: twilio_sms_inbox },
|
|
{ source_id: '1123', inbox: twilio_whatsapp_inbox }
|
|
])
|
|
expect(inbox_service).to receive(:get)
|
|
get "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/contactable_inboxes",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
# only the inboxes which agent has access to are shown
|
|
expect(response.parsed_body['payload'].pluck('inbox').pluck('id')).to eq([twilio_whatsapp_inbox.id])
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/contacts' do
|
|
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
|
let(:valid_params) { { name: 'test', custom_attributes: custom_attributes } }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
expect { post "/api/v1/accounts/#{account.id}/contacts", params: valid_params }.not_to change(Contact, :count)
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
|
|
it 'creates the contact' do
|
|
expect do
|
|
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
|
params: valid_params
|
|
end.to change(Contact, :count).by(1)
|
|
|
|
expect(response).to have_http_status(:success)
|
|
|
|
# custom attributes are updated
|
|
json_response = response.parsed_body
|
|
expect(json_response['payload']['contact']['custom_attributes']).to eq({ 'test' => 'test', 'test1' => 'test1' })
|
|
end
|
|
|
|
it 'does not create the contact' do
|
|
valid_params[:name] = 'test' * 999
|
|
|
|
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
|
params: valid_params
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
|
|
json_response = response.parsed_body
|
|
expect(json_response['message']).to eq('Name is too long (maximum is 255 characters)')
|
|
end
|
|
|
|
it 'creates the contact inbox when inbox id is passed' do
|
|
expect do
|
|
post "/api/v1/accounts/#{account.id}/contacts", headers: admin.create_new_auth_token,
|
|
params: valid_params.merge({ inbox_id: inbox.id })
|
|
end.to change(ContactInbox, :count).by(1)
|
|
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'PATCH /api/v1/accounts/{account.id}/contacts/:id' do
|
|
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
|
let(:additional_attributes) { { attr1: 'attr1', attr2: 'attr2' } }
|
|
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes, additional_attributes: additional_attributes) }
|
|
let(:valid_params) do
|
|
{ name: 'Test Blub', custom_attributes: { test: 'new test', test2: 'test2' }, additional_attributes: { attr2: 'new attr2', attr3: 'attr3' } }
|
|
end
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
put "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
params: valid_params
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'updates the contact' do
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token,
|
|
params: valid_params,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response).to conform_schema(200)
|
|
expect(contact.reload.name).to eq('Test Blub')
|
|
# custom attributes are merged properly without overwriting existing ones
|
|
expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' })
|
|
expect(contact.additional_attributes).to eq({ 'attr1' => 'attr1', 'attr2' => 'new attr2', 'attr3' => 'attr3' })
|
|
end
|
|
|
|
it 'prevents the update of contact of another account' do
|
|
other_account = create(:account)
|
|
other_contact = create(:contact, account: other_account)
|
|
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{other_contact.id}",
|
|
headers: admin.create_new_auth_token,
|
|
params: valid_params,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:not_found)
|
|
end
|
|
|
|
it 'prevents updating with an existing email' do
|
|
other_contact = create(:contact, account: account, email: 'test1@example.com')
|
|
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token,
|
|
params: valid_params.merge({ email: other_contact.email }),
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['attributes']).to include('email')
|
|
end
|
|
|
|
it 'prevents updating with an existing phone number' do
|
|
other_contact = create(:contact, account: account, phone_number: '+12000000')
|
|
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token,
|
|
params: valid_params.merge({ phone_number: other_contact.phone_number }),
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(response.parsed_body['attributes']).to include('phone_number')
|
|
end
|
|
|
|
it 'updates avatar' do
|
|
# no avatar before upload
|
|
expect(contact.avatar.attached?).to be(false)
|
|
file = fixture_file_upload(Rails.root.join('spec/assets/avatar.png'), 'image/png')
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
params: valid_params.merge(avatar: file),
|
|
headers: admin.create_new_auth_token
|
|
|
|
expect(response).to have_http_status(:success)
|
|
contact.reload
|
|
expect(contact.avatar.attached?).to be(true)
|
|
end
|
|
|
|
it 'updated avatar with avatar_url' do
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
params: valid_params.merge(avatar_url: 'http://example.com/avatar.png'),
|
|
headers: admin.create_new_auth_token
|
|
expect(response).to have_http_status(:success)
|
|
expect(Avatar::AvatarFromUrlJob).to have_been_enqueued.with(contact, 'http://example.com/avatar.png')
|
|
end
|
|
|
|
it 'allows blocking of contact' do
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
params: { blocked: true },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(contact.reload.blocked).to be(true)
|
|
end
|
|
|
|
it 'allows unblocking of contact' do
|
|
contact.update(blocked: true)
|
|
patch "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
params: { blocked: false },
|
|
headers: admin.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(contact.reload.blocked).to be(false)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'DELETE /api/v1/accounts/{account.id}/contacts/:id', :contact_delete do
|
|
let(:inbox) { create(:inbox, account: account) }
|
|
let(:contact) { create(:contact, account: account) }
|
|
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: inbox) }
|
|
let(:conversation) { create(:conversation, account: account, inbox: inbox, contact: contact, contact_inbox: contact_inbox) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
it 'deletes the contact for administrator user' do
|
|
allow(OnlineStatusTracker).to receive(:get_presence).and_return(false)
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token
|
|
|
|
expect(contact.conversations).to be_empty
|
|
expect(contact.inboxes).to be_empty
|
|
expect(contact.contact_inboxes).to be_empty
|
|
expect(contact.csat_survey_responses).to be_empty
|
|
expect { contact.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
|
|
it 'does not delete the contact if online' do
|
|
allow(OnlineStatusTracker).to receive(:get_presence).and_return(true)
|
|
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: admin.create_new_auth_token
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
|
|
it 'returns unauthorized for agent user' do
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}",
|
|
headers: agent.create_new_auth_token
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'POST /api/v1/accounts/{account.id}/contacts/:id/destroy_custom_attributes' do
|
|
let(:custom_attributes) { { test: 'test', test1: 'test1' } }
|
|
let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) }
|
|
let(:valid_params) { { custom_attributes: ['test'] } }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
|
|
params: valid_params
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
it 'delete the custom attribute' do
|
|
post "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/destroy_custom_attributes",
|
|
headers: admin.create_new_auth_token,
|
|
params: valid_params,
|
|
as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(contact.reload.custom_attributes).to eq({ 'test1' => 'test1' })
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'DELETE /api/v1/accounts/{account.id}/contacts/:id/avatar' do
|
|
let(:contact) { create(:contact, account: account) }
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
context 'when it is an unauthenticated user' do
|
|
it 'returns unauthorized' do
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/avatar"
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when it is an authenticated user' do
|
|
before do
|
|
create(:contact, account: account)
|
|
contact.avatar.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
|
|
end
|
|
|
|
it 'delete contact avatar' do
|
|
delete "/api/v1/accounts/#{account.id}/contacts/#{contact.id}/avatar",
|
|
headers: agent.create_new_auth_token,
|
|
as: :json
|
|
|
|
expect { contact.avatar.attachment.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
|
expect(response).to have_http_status(:success)
|
|
end
|
|
end
|
|
end
|
|
end
|