Files
leadchat/spec/controllers/public/api/v1/portals_controller_spec.rb
Vishnu Narayanan 4381be5f3e feat: disable helpcenter on hacker plans (#12068)
This change blocks Help Center access for default/Hacker-plan accounts
and closes the downgrade gap that could leave `help_center` enabled
after a subscription falls back to the default cloud plan.

Fixes: none
Closes: none

## Why

Default-plan accounts should not be able to access the Help Center, but
the downgrade fallback path only reset the plan name and did not
reconcile premium feature flags. That meant some accounts could keep
`help_center` enabled even after landing back on the Hacker/default
plan.

## What this change does

- blocks Help Center portal and article access for default/Hacker-plan
accounts
- reconciles premium feature flags when a subscription falls back to the
default cloud plan, so `help_center` is disabled immediately instead of
waiting for a later webhook
- preserves existing account `custom_attributes` during Stripe customer
recreation instead of overwriting them
- adds Enterprise coverage for the default-plan access checks on hosted
and custom-domain Help Center routes
- fixes the public access check to use the resolved portal object so
blocked requests return the intended response instead of raising an
error

## Validation

1. Create or use an account on the default/Hacker cloud plan with an
active portal.
2. Visit the portal home page and a published article on both the
Chatwoot-hosted URL and a configured custom domain.
3. Confirm the Help Center is blocked for that account.
4. Downgrade a paid account back to the default/Hacker plan through the
Stripe webhook flow.
5. Confirm `help_center` is disabled right after the downgrade fallback
is processed and the account can no longer access the Help Center.

---------

Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
Co-authored-by: Sojan Jose <sojan@pepalo.com>
2026-03-26 23:48:46 -07:00

144 lines
4.8 KiB
Ruby

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 'redirects to the portal default locale when locale is not present' do
get "/hc/#{portal.slug}"
expect(response).to redirect_to("/hc/#{portal.slug}/#{portal.default_locale}")
end
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
it 'hides drafted locales from the public locale switcher' do
portal.update!(config: { allowed_locales: %w[en es], draft_locales: ['es'], default_locale: 'en' })
get "/hc/#{portal.slug}/en"
expect(response).to have_http_status(:success)
expect(response.body).not_to include('value="es"')
expect(response.body).not_to include('locale-switcher')
end
it 'allows direct access to drafted locale pages' do
portal.update!(config: { allowed_locales: %w[en es], draft_locales: ['es'], default_locale: 'en' })
get "/hc/#{portal.slug}/es"
expect(response).to have_http_status(:success)
end
it 'shows the active drafted locale in the switcher state on direct locale access' do
portal.update!(config: { allowed_locales: %w[en es fr], draft_locales: ['es'], default_locale: 'en' })
get "/hc/#{portal.slug}/es"
expect(response).to have_http_status(:success)
document = Nokogiri::HTML(response.body)
switchers = document.css('select.locale-switcher')
expect(switchers).not_to be_empty
switchers.each do |switcher|
options = switcher.css('option')
expect(options.map { |option| option['value'] }).to include('en', 'es', 'fr')
expect(
options.any? do |option|
option['value'] == 'es' && option['selected'].present? && option['disabled'].present?
end
).to be(true)
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