require 'rails_helper' RSpec.describe Twilio::TemplateSyncService do subject(:sync_service) { described_class.new(channel: twilio_channel) } let!(:account) { create(:account) } let!(:twilio_channel) { create(:channel_twilio_sms, medium: :whatsapp, account: account) } let(:twilio_client) { instance_double(Twilio::REST::Client) } let(:content_api) { double } let(:contents_list) { double } # Mock Twilio template objects let(:text_template) do instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX123456789', friendly_name: 'hello_world', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/text' => { 'body' => 'Hello World!' } } ) end let(:media_template) do instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX987654321', friendly_name: 'product_showcase', language: 'en', date_created: Time.current, date_updated: Time.current, variables: { '1' => 'iPhone', '2' => '$999' }, types: { 'twilio/media' => { 'body' => 'Check out {{1}} for {{2}}', 'media' => ['https://example.com/image.jpg'] } } ) end let(:quick_reply_template) do instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX555666777', friendly_name: 'welcome_message', language: 'en_US', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/quick-reply' => { 'body' => 'Welcome! How can we help?', 'actions' => [ { 'id' => 'support', 'title' => 'Support' }, { 'id' => 'sales', 'title' => 'Sales' } ] } } ) end let(:catalog_template) do instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX111222333', friendly_name: 'product_catalog', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/catalog' => { 'body' => 'Check our catalog', 'catalog_id' => 'catalog123' } } ) end let(:templates) { [text_template, media_template, quick_reply_template, catalog_template] } before do allow(twilio_channel).to receive(:send).and_call_original allow(twilio_channel).to receive(:send).with(:client).and_return(twilio_client) allow(twilio_client).to receive(:content).and_return(content_api) allow(content_api).to receive(:v1).and_return(content_api) allow(content_api).to receive(:contents).and_return(contents_list) allow(contents_list).to receive(:list).with(limit: 1000).and_return(templates) end describe '#call' do context 'with successful sync' do it 'fetches templates from Twilio and updates the channel' do freeze_time do result = sync_service.call expect(result).to be_truthy expect(contents_list).to have_received(:list).with(limit: 1000) twilio_channel.reload expect(twilio_channel.content_templates).to be_present expect(twilio_channel.content_templates['templates']).to be_an(Array) expect(twilio_channel.content_templates['templates'].size).to eq(4) expect(twilio_channel.content_templates_last_updated).to be_within(1.second).of(Time.current) end end it 'correctly formats text templates' do sync_service.call twilio_channel.reload text_template_data = twilio_channel.content_templates['templates'].find do |t| t['friendly_name'] == 'hello_world' end expect(text_template_data).to include( 'content_sid' => 'HX123456789', 'friendly_name' => 'hello_world', 'language' => 'en', 'status' => 'approved', 'template_type' => 'text', 'media_type' => nil, 'variables' => {}, 'category' => 'utility', 'body' => 'Hello World!' ) end it 'correctly formats media templates' do sync_service.call twilio_channel.reload media_template_data = twilio_channel.content_templates['templates'].find do |t| t['friendly_name'] == 'product_showcase' end expect(media_template_data).to include( 'content_sid' => 'HX987654321', 'friendly_name' => 'product_showcase', 'language' => 'en', 'status' => 'approved', 'template_type' => 'media', 'media_type' => nil, # Would be derived from media content if present 'variables' => { '1' => 'iPhone', '2' => '$999' }, 'category' => 'utility', 'body' => 'Check out {{1}} for {{2}}' ) end it 'correctly formats quick reply templates' do sync_service.call twilio_channel.reload quick_reply_template_data = twilio_channel.content_templates['templates'].find do |t| t['friendly_name'] == 'welcome_message' end expect(quick_reply_template_data).to include( 'content_sid' => 'HX555666777', 'friendly_name' => 'welcome_message', 'language' => 'en_US', 'status' => 'approved', 'template_type' => 'quick_reply', 'media_type' => nil, 'variables' => {}, 'category' => 'utility', 'body' => 'Welcome! How can we help?' ) end it 'categorizes marketing templates correctly' do marketing_template = instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX_MARKETING', friendly_name: 'promo_offer_50_off', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/text' => { 'body' => '50% off sale!' } } ) allow(contents_list).to receive(:list).with(limit: 1000).and_return([marketing_template]) sync_service.call twilio_channel.reload marketing_data = twilio_channel.content_templates['templates'].first expect(marketing_data['category']).to eq('marketing') end it 'categorizes authentication templates correctly' do auth_template = instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX_AUTH', friendly_name: 'otp_verification', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/text' => { 'body' => 'Your OTP is {{1}}' } } ) allow(contents_list).to receive(:list).with(limit: 1000).and_return([auth_template]) sync_service.call twilio_channel.reload auth_data = twilio_channel.content_templates['templates'].first expect(auth_data['category']).to eq('authentication') end end context 'with API error' do before do allow(contents_list).to receive(:list).and_raise(Twilio::REST::TwilioError.new('API Error')) allow(Rails.logger).to receive(:error) end it 'handles Twilio::REST::TwilioError gracefully' do result = sync_service.call expect(result).to be_falsey expect(Rails.logger).to have_received(:error).with('Twilio template sync failed: API Error') end end context 'with generic error' do before do allow(contents_list).to receive(:list).and_raise(StandardError, 'Connection failed') allow(Rails.logger).to receive(:error) end it 'propagates non-Twilio errors' do expect { sync_service.call }.to raise_error(StandardError, 'Connection failed') end end context 'with empty templates list' do before do allow(contents_list).to receive(:list).with(limit: 1000).and_return([]) end it 'updates channel with empty templates array' do sync_service.call twilio_channel.reload expect(twilio_channel.content_templates['templates']).to eq([]) expect(twilio_channel.content_templates_last_updated).to be_present end end end describe 'template categorization behavior' do it 'defaults to utility category for unrecognized patterns' do generic_template = instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX_GENERIC', friendly_name: 'order_status', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/text' => { 'body' => 'Order updated' } } ) allow(contents_list).to receive(:list).with(limit: 1000).and_return([generic_template]) sync_service.call twilio_channel.reload template_data = twilio_channel.content_templates['templates'].first expect(template_data['category']).to eq('utility') end end describe 'template type detection' do context 'with multiple type definitions' do let(:mixed_template) do instance_double( Twilio::REST::Content::V1::ContentInstance, sid: 'HX_MIXED', friendly_name: 'mixed_type', language: 'en', date_created: Time.current, date_updated: Time.current, variables: {}, types: { 'twilio/media' => { 'body' => 'Media content' }, 'twilio/text' => { 'body' => 'Text content' } } ) end before do allow(contents_list).to receive(:list).with(limit: 1000).and_return([mixed_template]) end it 'prioritizes media type for type detection but text for body extraction' do sync_service.call twilio_channel.reload template_data = twilio_channel.content_templates['templates'].first # derive_template_type prioritizes media expect(template_data['template_type']).to eq('media') # but extract_body_content prioritizes text expect(template_data['body']).to eq('Text content') end end end end