Merge branch 'develop' into feat/idb-push-invalidation

This commit is contained in:
Sivin Varghese
2026-06-10 23:32:27 +05:30
committed by GitHub
25 changed files with 210 additions and 376 deletions
@@ -111,4 +111,40 @@ RSpec.describe 'Onboarding API', type: :request do
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
@@ -122,8 +122,8 @@ RSpec.describe SamlUserBuilder do
it 'does not add the user to the target account' do
expect do
builder.perform
rescue SamlUserBuilder::AuthenticationFailed
nil
rescue StandardError => e
raise unless e.class.name == 'SamlUserBuilder::AuthenticationFailed' # rubocop:disable Style/ClassEqualityComparison
end.not_to change(AccountUser, :count)
expect(existing_user.reload.accounts).not_to include(account)
end
@@ -131,8 +131,8 @@ RSpec.describe SamlUserBuilder do
it 'does not convert the user provider to saml' do
expect do
builder.perform
rescue SamlUserBuilder::AuthenticationFailed
nil
rescue StandardError => e
raise unless e.class.name == 'SamlUserBuilder::AuthenticationFailed' # rubocop:disable Style/ClassEqualityComparison
end.not_to(change { existing_user.reload.provider })
end
end
@@ -0,0 +1,42 @@
require 'rails_helper'
RSpec.describe 'Enterprise Onboarding API', type: :request do
let(:account) { create(:account, domain: 'example.com') }
let(:admin) { create(:user, account: account, role: :administrator) }
describe 'GET /api/v1/accounts/{account.id}/onboarding/help_center_generation' do
context 'when help center generation is in progress' do
let(:generation_id) { 'generation-123' }
let!(:portal) { create(:portal, account_id: account.id) }
let!(:category) { create(:category, portal: portal, account_id: account.id) }
before do
account.update!(custom_attributes: { 'help_center_generation_id' => generation_id })
create(:article, portal: portal, category: category, account_id: account.id, author_id: admin.id)
Onboarding::HelpCenterGenerationState.start(generation_id, total: 3)
Onboarding::HelpCenterGenerationState.record_article_finished(generation_id)
end
after do
Redis::Alfred.delete(Onboarding::HelpCenterGenerationState.key(generation_id))
end
it 'returns Redis state and help center 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' => generation_id,
'articles_count' => 1,
'categories_count' => 1
)
expect(response.parsed_body['state']).to include(
'status' => 'generating',
'finished' => '1',
'total' => '3'
)
end
end
end
end
@@ -53,11 +53,9 @@ RSpec.describe Onboarding::HelpCenterArticleGenerationJob do
admin.id,
generation_id,
hash_including(
'article' => hash_including(
'title' => 'Hello',
'urls' => ['https://x.test/a'],
'category_id' => portal.categories.first.id
)
'title' => 'Hello',
'urls' => ['https://x.test/a'],
'category_id' => portal.categories.first.id
)
)
)
@@ -83,7 +81,7 @@ RSpec.describe Onboarding::HelpCenterArticleGenerationJob do
writer_jobs = enqueued_jobs.select { |job| job['job_class'] == Onboarding::HelpCenterArticleWriterJob.name }
expect(writer_jobs.size).to eq(1)
expect(writer_jobs.first['arguments']).to include(
hash_including('article' => hash_including('title' => 'Valid'))
hash_including('title' => 'Valid')
)
end
end
@@ -106,7 +104,7 @@ RSpec.describe Onboarding::HelpCenterArticleGenerationJob do
writer_jobs = enqueued_jobs.select { |job| job['job_class'] == Onboarding::HelpCenterArticleWriterJob.name }
expect(writer_jobs.size).to eq(1)
expect(writer_jobs.first['arguments']).to include(
hash_including('article' => hash_including('title' => 'Approved', 'urls' => ['https://x.test/a']))
hash_including('title' => 'Approved', 'urls' => ['https://x.test/a'])
)
expect(Onboarding::HelpCenterGenerationState.current(generation_id)).to include('total' => '1')
end
@@ -170,19 +168,4 @@ RSpec.describe Onboarding::HelpCenterArticleGenerationJob do
expect(state['skip_reason']).to include('firecrawl exhausted')
end
end
describe 'broadcasts' do
it 'broadcasts generation_completed with status: skipped on CurationSkipped' do
curator = instance_double(Onboarding::HelpCenterCurator)
allow(curator).to receive(:perform).and_raise(
Onboarding::HelpCenterErrors::CurationSkipped, 'no website url'
)
allow(Onboarding::HelpCenterCurator).to receive(:new).and_return(curator)
payload = hash_including(generation_id: generation_id, status: 'skipped', skip_reason: 'no website url')
expect { described_class.perform_now(*job_args) }
.to have_enqueued_job(ActionCableBroadcastJob)
.with([admin.pubsub_token], 'help_center.generation_completed', payload)
end
end
end
@@ -6,8 +6,7 @@ RSpec.describe Onboarding::HelpCenterArticleWriterJob do
let!(:admin) { create(:user, account: account, role: :administrator) }
let(:generation_id) { 'generation-123' }
let(:article_spec) { { 'urls' => ['https://x.test/a'], 'title' => 'A', 'category_id' => nil } }
let(:article_payload) { { 'article' => article_spec } }
let(:job_args) { [account.id, portal.id, admin.id, generation_id, article_payload] }
let(:job_args) { [account.id, portal.id, admin.id, generation_id, article_spec] }
let(:state_key) { Onboarding::HelpCenterGenerationState.key(generation_id) }
before do
@@ -68,16 +67,14 @@ RSpec.describe Onboarding::HelpCenterArticleWriterJob do
expect(Onboarding::HelpCenterGenerationState.current(generation_id)).to include('finished' => '1')
end
it 'broadcasts completion when the final writer fails with ArticleBuildFailed' do
it 'marks generation completed when the final writer fails with ArticleBuildFailed' do
allow(Onboarding::HelpCenterArticleBuilder).to receive(:new).and_raise(
Onboarding::HelpCenterErrors::ArticleBuildFailed, 'no source urls'
)
Onboarding::HelpCenterGenerationState.record_article_finished(generation_id)
payload = hash_including(generation_id: generation_id, status: 'completed')
expect { described_class.perform_now(*job_args) }
.to have_enqueued_job(ActionCableBroadcastJob)
.with([admin.pubsub_token], 'help_center.generation_completed', payload)
described_class.perform_now(*job_args)
expect(Onboarding::HelpCenterGenerationState.current(generation_id)).to include(
'status' => 'completed', 'finished' => '2'
)
@@ -105,7 +102,7 @@ RSpec.describe Onboarding::HelpCenterArticleWriterJob do
end
end
describe 'broadcasts' do
describe 'missing state' do
let(:built_article) { instance_double(Article, id: 9876) }
before do
@@ -113,47 +110,10 @@ RSpec.describe Onboarding::HelpCenterArticleWriterJob do
allow(Onboarding::HelpCenterArticleBuilder).to receive(:new).and_return(builder)
end
it 'broadcasts help_center.article_generated on success' do
payload = hash_including(generation_id: generation_id, article_id: 9876, articles_finished: 1)
expect { described_class.perform_now(*job_args) }
.to have_enqueued_job(ActionCableBroadcastJob)
.with([admin.pubsub_token], 'help_center.article_generated', payload)
end
it 'broadcasts help_center.generation_completed when the last writer finishes' do
described_class.perform_now(*job_args)
payload = hash_including(generation_id: generation_id, status: 'completed')
expect { described_class.perform_now(*job_args) }
.to have_enqueued_job(ActionCableBroadcastJob)
.with([admin.pubsub_token], 'help_center.generation_completed', payload)
end
it 'does not broadcast article_generated on builder failure' do
allow(Onboarding::HelpCenterArticleBuilder).to receive(:new).and_raise(
Onboarding::HelpCenterErrors::ArticleBuildFailed, 'no source urls'
)
expect { described_class.perform_now(*job_args) }
.not_to have_enqueued_job(ActionCableBroadcastJob)
.with(anything, 'help_center.article_generated', anything)
end
it 'broadcasts generation_completed on late retries past total' do
described_class.perform_now(*job_args)
described_class.perform_now(*job_args)
clear_enqueued_jobs
expect { described_class.perform_now(*job_args) }
.to have_enqueued_job(ActionCableBroadcastJob)
.with([admin.pubsub_token], 'help_center.generation_completed', hash_including(generation_id: generation_id))
end
it 'skips progress broadcasts when state is missing' do
it 'does not raise when state is missing' do
Redis::Alfred.delete(state_key)
expect { described_class.perform_now(*job_args) }
.not_to have_enqueued_job(ActionCableBroadcastJob)
expect { described_class.perform_now(*job_args) }.not_to raise_error
end
end
end
+2 -2
View File
@@ -58,7 +58,7 @@ RSpec.describe MutexApplicationJob do
describe '.retry_on_lock_conflict' do
let(:job_class) do
Class.new(described_class) do
Class.new(MutexApplicationJob) do
retry_on_lock_conflict wait: 1.second, attempts: 1, on_exhaustion: :process_without_lock
attr_reader :fallback_args
@@ -90,7 +90,7 @@ RSpec.describe MutexApplicationJob do
context 'without an exhaustion handler' do
let(:job_class) do
Class.new(described_class) do
Class.new(MutexApplicationJob) do
retry_on_lock_conflict wait: 1.second, attempts: 1
def perform(lock_key)
@@ -36,11 +36,6 @@ describe Whatsapp::EmbeddedSignupService do
.with(params[:waba_id], params[:phone_number_id], access_token).and_return(phone_service)
allow(phone_service).to receive(:perform).and_return(phone_info)
validation_service = instance_double(Whatsapp::TokenValidationService)
allow(Whatsapp::TokenValidationService).to receive(:new)
.with(access_token, params[:waba_id]).and_return(validation_service)
allow(validation_service).to receive(:perform)
channel_creation = instance_double(Whatsapp::ChannelCreationService)
allow(Whatsapp::ChannelCreationService).to receive(:new)
.with(account, { waba_id: params[:waba_id], business_name: 'Test Business' }, phone_info, access_token)
@@ -1,99 +0,0 @@
require 'rails_helper'
describe Whatsapp::TokenValidationService do
let(:access_token) { 'test_access_token' }
let(:waba_id) { 'test_waba_id' }
let(:service) { described_class.new(access_token, waba_id) }
let(:api_client) { instance_double(Whatsapp::FacebookApiClient) }
before do
allow(Whatsapp::FacebookApiClient).to receive(:new).with(access_token).and_return(api_client)
end
describe '#perform' do
context 'when token has access to WABA' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'whatsapp_business_management',
'target_ids' => [waba_id, 'another_waba_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'validates successfully' do
expect { service.perform }.not_to raise_error
end
end
context 'when token does not have access to WABA' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'whatsapp_business_management',
'target_ids' => ['different_waba_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error(/Token does not have access to WABA/)
end
end
context 'when no WABA scope is found' do
let(:debug_response) do
{
'data' => {
'granular_scopes' => [
{
'scope' => 'some_other_scope',
'target_ids' => ['some_id']
}
]
}
}
end
before do
allow(api_client).to receive(:debug_token).with(access_token).and_return(debug_response)
end
it 'raises an error' do
expect { service.perform }.to raise_error('No WABA scope found in token')
end
end
context 'when access_token is blank' do
let(:access_token) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'Access token is required')
end
end
context 'when waba_id is blank' do
let(:waba_id) { '' }
it 'raises ArgumentError' do
expect { service.perform }.to raise_error(ArgumentError, 'WABA ID is required')
end
end
end
end