After entering their account details, new admins land on an **Inbox setup** screen that shows what we've already set up for them and lets them connect their conversation channels without leaving onboarding. It surfaces the auto-created live chat widget (and Help Center on Enterprise), highlights channels detected from their website, and offers a **View all** dialog to connect any supported channel inline. ### Channel status | Channel | How it connects | Status | PR | |---|---|---|---| | Live chat (Website) | Auto-created during setup | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14314 | | WhatsApp | Meta embedded signup | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14619 | | Facebook | Login + page picker | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14619 | | Instagram | OAuth redirect | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14568 | | TikTok | OAuth redirect | ✅ Done | https://github.com/chatwoot/chatwoot/pull/14569 | | LINE | Inline credential form | ✅ Done | — | | Telegram | Inline credential form | ✅ Done | — | | Gmail / Outlook | OAuth (email) | ⚠️ Disabled — coming in a follow-up | https://github.com/chatwoot/chatwoot/pull/14567 | | SMS / API / Voice / Other email | — | ⛔ Unavailable | — | --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com>
238 lines
9.5 KiB
Ruby
238 lines
9.5 KiB
Ruby
require 'rails_helper'
|
|
|
|
RSpec.describe 'Onboarding API', type: :request do
|
|
let(:account) { create(:account, domain: 'example.com') }
|
|
let(:admin) { create(:user, account: account, role: :administrator) }
|
|
|
|
describe 'PATCH /api/v1/accounts/{account.id}/onboarding' do
|
|
context 'when unauthenticated' do
|
|
it 'returns unauthorized' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding", params: { website: 'acme.com' }, as: :json
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when authenticated as an agent (non-admin)' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
it 'returns unauthorized and does not change the account' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { name: 'Hijacked', website: 'attacker.com' },
|
|
headers: agent.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
expect(account.reload.name).not_to eq('Hijacked')
|
|
end
|
|
|
|
it 'does not create a help center portal' do
|
|
account.update!(custom_attributes: { 'onboarding_step' => 'account_details' })
|
|
|
|
expect do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'attacker.com' },
|
|
headers: agent.create_new_auth_token, as: :json
|
|
end.not_to change(account.portals, :count)
|
|
end
|
|
end
|
|
|
|
context 'when finalizing account_details' do
|
|
before { account.update!(custom_attributes: { 'onboarding_step' => 'account_details' }) }
|
|
|
|
it 'saves name and locale' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { name: 'Acme Inc', locale: 'fr', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(account.reload.name).to eq('Acme Inc')
|
|
expect(account.locale).to eq('fr')
|
|
end
|
|
|
|
it 'merges custom_attributes' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com', industry: 'tech', company_size: '10-50', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
attrs = account.reload.custom_attributes
|
|
expect(attrs['website']).to eq('acme.com')
|
|
expect(attrs['industry']).to eq('tech')
|
|
expect(attrs['company_size']).to eq('10-50')
|
|
end
|
|
|
|
context 'when on cloud (inbox setup is a cloud-only step)' do
|
|
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true) }
|
|
|
|
it 'advances onboarding_step to inbox_setup' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(account.reload.custom_attributes['onboarding_step']).to eq('inbox_setup')
|
|
end
|
|
|
|
it 'does not create a help center portal when website is blank' do
|
|
expect do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { name: 'Acme Inc', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end.not_to change(account.portals, :count)
|
|
end
|
|
|
|
it 'is idempotent when the account_details completion is replayed' do
|
|
2.times do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end
|
|
|
|
# Replaying step 1 always lands on inbox_setup; it never skips to done.
|
|
expect(account.reload.custom_attributes['onboarding_step']).to eq('inbox_setup')
|
|
end
|
|
end
|
|
|
|
context 'when off cloud (inbox setup is skipped)' do
|
|
before { allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false) }
|
|
|
|
it 'finishes onboarding instead of advancing to inbox_setup' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
|
|
end
|
|
|
|
it 'does not auto-create onboarding inboxes' do
|
|
expect(Onboarding::WebWidgetCreationService).not_to receive(:new)
|
|
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when replaying account_details after onboarding has finished' do
|
|
before { account.update!(custom_attributes: { 'website' => 'acme.com' }) }
|
|
|
|
it 'does not re-enter onboarding or persist the stale payload' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'stale.com', onboarding_step: 'account_details' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
|
|
expect(account.custom_attributes['website']).to eq('acme.com')
|
|
end
|
|
end
|
|
|
|
context 'when finalizing inbox_setup' do
|
|
before { account.update!(custom_attributes: { 'onboarding_step' => 'inbox_setup' }) }
|
|
|
|
it 'clears onboarding_step' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { onboarding_step: 'inbox_setup' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
|
|
end
|
|
|
|
it 'does not create another web widget inbox' do
|
|
expect(Onboarding::WebWidgetCreationService).not_to receive(:new)
|
|
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { onboarding_step: 'inbox_setup' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end
|
|
|
|
it 'is idempotent when the finalize request is replayed' do
|
|
2.times do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { onboarding_step: 'inbox_setup' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end
|
|
|
|
expect(account.reload.custom_attributes).not_to have_key('onboarding_step')
|
|
end
|
|
end
|
|
|
|
context 'when the declared onboarding_step is missing or unknown' do
|
|
before { account.update!(custom_attributes: { 'onboarding_step' => 'invite_team' }) }
|
|
|
|
it 'rejects a request without an onboarding_step and changes nothing' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_team')
|
|
expect(account.custom_attributes['website']).to be_nil
|
|
end
|
|
|
|
it 'rejects an unknown onboarding_step' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { onboarding_step: 'invite_team' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:unprocessable_entity)
|
|
end
|
|
|
|
it 'does not create a help center portal' do
|
|
expect do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { website: 'acme.com' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
end.not_to change(account.portals, :count)
|
|
end
|
|
end
|
|
|
|
context 'when completing inbox_setup out of order' do
|
|
before { account.update!(custom_attributes: { 'onboarding_step' => 'account_details' }) }
|
|
|
|
it 'does not clear onboarding_step while the account is still on account_details' do
|
|
patch "/api/v1/accounts/#{account.id}/onboarding",
|
|
params: { onboarding_step: 'inbox_setup' },
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(account.reload.custom_attributes['onboarding_step']).to eq('account_details')
|
|
end
|
|
end
|
|
end
|
|
|
|
describe 'GET /api/v1/accounts/{account.id}/onboarding/help_center_generation' do
|
|
context 'when unauthenticated' do
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/onboarding/help_center_generation", as: :json
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when authenticated as an agent (non-admin)' do
|
|
let(:agent) { create(:user, account: account, role: :agent) }
|
|
|
|
it 'returns unauthorized' do
|
|
get "/api/v1/accounts/#{account.id}/onboarding/help_center_generation",
|
|
headers: agent.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:unauthorized)
|
|
end
|
|
end
|
|
|
|
context 'when no help center generation has started' do
|
|
it 'returns not_started with zero counts' do
|
|
get "/api/v1/accounts/#{account.id}/onboarding/help_center_generation",
|
|
headers: admin.create_new_auth_token, as: :json
|
|
|
|
expect(response).to have_http_status(:success)
|
|
expect(response.parsed_body).to include(
|
|
'generation_id' => nil,
|
|
'state' => nil,
|
|
'articles_count' => 0,
|
|
'categories_count' => 0
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|