Instruction:
Write RSpec test file for following ruby class
```ruby
class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService
  def send_message(phone_number, message)
    if message.attachments.present?
      send_attachment_message(phone_number, message)
    elsif message.content_type == 'input_select'
      send_interactive_text_message(phone_number, message)
    else
      send_text_message(phone_number, message)
    end
  end

  def send_template(phone_number, template_info)
    response = HTTParty.post(
      "#{phone_id_path}/messages",
      headers: api_headers,
      body: {
        messaging_product: 'whatsapp',
        to: phone_number,
        template: template_body_parameters(template_info),
        type: 'template'
      }.to_json
    )

    process_response(response)
  end

  def sync_templates
    # ensuring that channels with wrong provider config wouldn't keep trying to sync templates
    whatsapp_channel.mark_message_templates_updated
    templates = fetch_whatsapp_templates("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
    whatsapp_channel.update(message_templates: templates, message_templates_last_updated: Time.now.utc) if templates.present?
  end

  def fetch_whatsapp_templates(url)
    response = HTTParty.get(url)
    return [] unless response.success?

    next_url = next_url(response)

    return response['data'] + fetch_whatsapp_templates(next_url) if next_url.present?

    response['data']
  end

  def next_url(response)
    response['paging'] ? response['paging']['next'] : ''
  end

  def validate_provider_config?
    response = HTTParty.get("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
    response.success?
  end

  def api_headers
    { 'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}", 'Content-Type' => 'application/json' }
  end

  def media_url(media_id)
    "#{api_base_path}/v13.0/#{media_id}"
  end

  def api_base_path
    ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com')
  end

  # TODO: See if we can unify the API versions and for both paths and make it consistent with out facebook app API versions
  def phone_id_path
    "#{api_base_path}/v13.0/#{whatsapp_channel.provider_config['phone_number_id']}"
  end

  def business_account_path
    "#{api_base_path}/v14.0/#{whatsapp_channel.provider_config['business_account_id']}"
  end

  def send_text_message(phone_number, message)
    response = HTTParty.post(
      "#{phone_id_path}/messages",
      headers: api_headers,
      body: {
        messaging_product: 'whatsapp',
        context: whatsapp_reply_context(message),
        to: phone_number,
        text: { body: message.content },
        type: 'text'
      }.to_json
    )

    process_response(response)
  end

  def send_attachment_message(phone_number, message)
    attachment = message.attachments.first
    type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
    type_content = {
      'link': attachment.download_url
    }
    type_content['caption'] = message.content unless %w[audio sticker].include?(type)
    type_content['filename'] = attachment.file.filename if type == 'document'
    response = HTTParty.post(
      "#{phone_id_path}/messages",
      headers: api_headers,
      body: {
        :messaging_product => 'whatsapp',
        :context => whatsapp_reply_context(message),
        'to' => phone_number,
        'type' => type,
        type.to_s => type_content
      }.to_json
    )

    process_response(response)
  end

  def process_response(response)
    if response.success?
      response['messages'].first['id']
    else
      Rails.logger.error response.body
      nil
    end
  end

  def template_body_parameters(template_info)
    {
      name: template_info[:name],
      language: {
        policy: 'deterministic',
        code: template_info[:lang_code]
      },
      components: [{
        type: 'body',
        parameters: template_info[:parameters]
      }]
    }
  end

  def whatsapp_reply_context(message)
    reply_to = message.content_attributes[:in_reply_to_external_id]
    return nil if reply_to.blank?

    {
      message_id: reply_to
    }
  end

  def send_interactive_text_message(phone_number, message)
    payload = create_payload_based_on_items(message)

    response = HTTParty.post(
      "#{phone_id_path}/messages",
      headers: api_headers,
      body: {
        messaging_product: 'whatsapp',
        to: phone_number,
        interactive: payload,
        type: 'interactive'
      }.to_json
    )

    process_response(response)
  end
end

```

Response:
require 'rails_helper'

describe Whatsapp::Providers::WhatsappCloudService do
  subject(:service) { described_class.new(whatsapp_channel: whatsapp_channel) }

  let(:conversation) { create(:conversation, inbox: whatsapp_channel.inbox) }
  let(:whatsapp_channel) { create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) }

  let(:message) do
    create(:message, conversation: conversation, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox, source_id: 'external_id')
  end

  let(:message_with_reply) do
    create(:message, conversation: conversation, message_type: :outgoing, content: 'reply', inbox: whatsapp_channel.inbox,
                     content_attributes: { in_reply_to: message.id })
  end

  let(:response_headers) { { 'Content-Type' => 'application/json' } }
  let(:whatsapp_response) { { messages: [{ id: 'message_id' }] } }

  before do
    stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
  end

  describe '#send_message' do
    context 'when called' do
      it 'calls message endpoints for normal messages' do
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: {
              messaging_product: 'whatsapp',
              context: nil,
              to: '+123456789',
              text: { body: message.content },
              type: 'text'
            }.to_json
          )
          .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message)).to eq 'message_id'
      end

      it 'calls message endpoints for a reply to messages' do
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: {
              messaging_product: 'whatsapp',
              context: {
                message_id: message.source_id
              },
              to: '+123456789',
              text: { body: message_with_reply.content },
              type: 'text'
            }.to_json
          )
          .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message_with_reply)).to eq 'message_id'
      end

      it 'calls message endpoints for image attachment message messages' do
        attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
        attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')

        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: hash_including({
                                   messaging_product: 'whatsapp',
                                   to: '+123456789',
                                   type: 'image',
                                   image: WebMock::API.hash_including({ caption: message.content, link: anything })
                                 })
          )
          .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message)).to eq 'message_id'
      end

      it 'calls message endpoints for document attachment message messages' do
        attachment = message.attachments.new(account_id: message.account_id, file_type: :file)
        attachment.file.attach(io: Rails.root.join('spec/assets/sample.pdf').open, filename: 'sample.pdf', content_type: 'application/pdf')

        # ref: https://github.com/bblimke/webmock/issues/900
        # reason for Webmock::API.hash_including
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: hash_including({
                                   messaging_product: 'whatsapp',
                                   to: '+123456789',
                                   type: 'document',
                                   document: WebMock::API.hash_including({ filename: 'sample.pdf', caption: message.content, link: anything })
                                 })
          )
          .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message)).to eq 'message_id'
      end
    end
  end

  describe '#send_interactive message' do
    context 'when called' do
      it 'calls message endpoints with button payload when number of items is less than or equal to 3' do
        message = create(:message, message_type: :outgoing, content: 'test',
                                   inbox: whatsapp_channel.inbox, content_type: 'input_select',
                                   content_attributes: {
                                     items: [
                                       { title: 'Burito', value: 'Burito' },
                                       { title: 'Pasta', value: 'Pasta' },
                                       { title: 'Sushi', value: 'Sushi' }
                                     ]
                                   })
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: {
              messaging_product: 'whatsapp', to: '+123456789',
              interactive: {
                type: 'button',
                body: {
                  text: 'test'
                },
                action: '{"buttons":[{"type":"reply","reply":{"id":"Burito","title":"Burito"}},{"type":"reply",' \
                        '"reply":{"id":"Pasta","title":"Pasta"}},{"type":"reply","reply":{"id":"Sushi","title":"Sushi"}}]}'
              }, type: 'interactive'
            }.to_json
          ).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message)).to eq 'message_id'
      end

      it 'calls message endpoints with list payload when number of items is greater than 3' do
        message = create(:message, message_type: :outgoing, content: 'test', inbox: whatsapp_channel.inbox,
                                   content_type: 'input_select',
                                   content_attributes: {
                                     items: [
                                       { title: 'Burito', value: 'Burito' },
                                       { title: 'Pasta', value: 'Pasta' },
                                       { title: 'Sushi', value: 'Sushi' },
                                       { title: 'Salad', value: 'Salad' }
                                     ]
                                   })
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: {
              messaging_product: 'whatsapp', to: '+123456789',
              interactive: {
                type: 'list',
                body: {
                  text: 'test'
                },
                action: '{"button":"Choose an item","sections":[{"rows":[{"id":"Burito","title":"Burito"},' \
                        '{"id":"Pasta","title":"Pasta"},{"id":"Sushi","title":"Sushi"},{"id":"Salad","title":"Salad"}]}]}'
              }, type: 'interactive'
            }.to_json
          ).to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
        expect(service.send_message('+123456789', message)).to eq 'message_id'
      end
    end
  end

  describe '#send_template' do
    let(:template_info) do
      {
        name: 'test_template',
        namespace: 'test_namespace',
        lang_code: 'en_US',
        parameters: [{ type: 'text', text: 'test' }]
      }
    end

    let(:template_body) do
      {
        messaging_product: 'whatsapp',
        to: '+123456789',
        template: {
          name: template_info[:name],
          language: {
            policy: 'deterministic',
            code: template_info[:lang_code]
          },
          components: [
            { type: 'body',
              parameters: template_info[:parameters] }
          ]
        },
        type: 'template'
      }
    end

    context 'when called' do
      it 'calls message endpoints with template params for template messages' do
        stub_request(:post, 'https://graph.facebook.com/v13.0/123456789/messages')
          .with(
            body: template_body.to_json
          )
          .to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)

        expect(service.send_template('+123456789', template_info)).to eq('message_id')
      end
    end
  end

  describe '#sync_templates' do
    context 'when called' do
      it 'updated the message templates' do
        stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
          .to_return(
            { status: 200, headers: response_headers,
              body: { data: [
                { id: '123456789', name: 'test_template' }
              ], paging: { next: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json },
            { status: 200, headers: response_headers,
              body: { data: [
                { id: '123456789', name: 'next_template' }
              ], paging: { next: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json },
            { status: 200, headers: response_headers,
              body: { data: [
                { id: '123456789', name: 'last_template' }
              ], paging: { prev: 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key' } }.to_json }
          )

        timstamp = whatsapp_channel.reload.message_templates_last_updated
        expect(subject.sync_templates).to be(true)
        expect(whatsapp_channel.reload.message_templates.first).to eq({ id: '123456789', name: 'test_template' }.stringify_keys)
        expect(whatsapp_channel.reload.message_templates.second).to eq({ id: '123456789', name: 'next_template' }.stringify_keys)
        expect(whatsapp_channel.reload.message_templates.last).to eq({ id: '123456789', name: 'last_template' }.stringify_keys)
        expect(whatsapp_channel.reload.message_templates_last_updated).not_to eq(timstamp)
      end

      it 'updates message_templates_last_updated even when template request fails' do
        stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
          .to_return(status: 401)

        timstamp = whatsapp_channel.reload.message_templates_last_updated
        subject.sync_templates
        expect(whatsapp_channel.reload.message_templates_last_updated).not_to eq(timstamp)
      end
    end
  end

  describe '#validate_provider_config' do
    context 'when called' do
      it 'returns true if valid' do
        stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key')
        expect(subject.validate_provider_config?).to be(true)
        expect(whatsapp_channel.errors.present?).to be(false)
      end

      it 'returns false if invalid' do
        stub_request(:get, 'https://graph.facebook.com/v14.0/123456789/message_templates?access_token=test_key').to_return(status: 401)
        expect(subject.validate_provider_config?).to be(false)
      end
    end
  end

  describe 'Ability to configure Base URL' do
    context 'when environment variable WHATSAPP_CLOUD_BASE_URL is not set' do
      it 'uses the default base url' do
        expect(subject.send(:api_base_path)).to eq('https://graph.facebook.com')
      end
    end

    context 'when environment variable WHATSAPP_CLOUD_BASE_URL is set' do
      it 'uses the base url from the environment variable' do
        with_modified_env WHATSAPP_CLOUD_BASE_URL: 'http://test.com' do
          expect(subject.send(:api_base_path)).to eq('http://test.com')
        end
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Twilio::DeliveryStatusService
  pattr_initialize [:params!]
  # Reference: https://www.twilio.com/docs/messaging/api/message-resource#message-status-values

  def perform
    return if twilio_channel.blank?

    return unless supported_status?

    process_statuses if message.present?
  end

  private

  def process_statuses
    @message.status = status
    @message.external_error = external_error if error_occurred?
    @message.save!
  end

  def supported_status?
    %w[sent delivered read failed undelivered].include?(params[:MessageStatus])
  end

  def status
    params[:MessageStatus] == 'undelivered' ? 'failed' : params[:MessageStatus]
  end

  def external_error
    return nil unless error_occurred?

    error_message = params[:ErrorMessage].presence
    error_code = params[:ErrorCode]

    if error_message.present?
      "#{error_code} - #{error_message}"
    elsif error_code.present?
      I18n.t('conversations.messages.delivery_status.error_code', error_code: error_code)
    end
  end

  def error_occurred?
    params[:ErrorCode].present? && %w[failed undelivered].include?(params[:MessageStatus])
  end

  def twilio_channel
    @twilio_channel ||= if params[:MessagingServiceSid].present?
                          ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid])
                        elsif params[:AccountSid].present? && params[:From].present?
                          ::Channel::TwilioSms.find_by!(account_sid: params[:AccountSid], phone_number: params[:From])
                        end
  end

  def message
    return unless params[:MessageSid]

    @message ||= twilio_channel.inbox.messages.find_by(source_id: params[:MessageSid])
  end
end

```

Response:
require 'rails_helper'

describe Twilio::DeliveryStatusService do
  let!(:account) { create(:account) }
  let!(:twilio_channel) do
    create(:channel_twilio_sms, account: account, account_sid: 'ACxxx',
                                inbox: create(:inbox, account: account, greeting_enabled: false))
  end
  let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
  let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: twilio_channel.inbox) }
  let!(:conversation) { create(:conversation, contact: contact, inbox: twilio_channel.inbox, contact_inbox: contact_inbox) }

  describe '#perform' do
    context 'when message delivery status is fired' do
      before do
        create(:message, account: account, inbox: twilio_channel.inbox, conversation: conversation, status: :sent,
                         source_id: 'SMd560ac79e4a4d36b3ce59f1f50471986')
      end

      it 'updates the message if the status is delivered' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'delivered'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('delivered')
      end

      it 'updates the message if the status is read' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'read'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('read')
      end

      it 'does not update the message if the status is not a support status' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'queued'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('sent')
      end

      it 'updates message status to failed if message status is undelivered' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'undelivered',
          ErrorCode: '30002',
          ErrorMessage: 'Account suspended'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('failed')
        expect(conversation.reload.messages.last.external_error).to eq('30002 - Account suspended')
      end

      it 'updates message status to failed and updates the error message if message status is failed' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'failed',
          ErrorCode: '30008',
          ErrorMessage: 'Unknown error'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('failed')
        expect(conversation.reload.messages.last.external_error).to eq('30008 - Unknown error')
      end

      it 'updates the error message if message status is undelivered and error message is not present' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          MessagingServiceSid: twilio_channel.messaging_service_sid,
          MessageSid: conversation.messages.last.source_id,
          MessageStatus: 'failed',
          ErrorCode: '30008'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.status).to eq('failed')
        expect(conversation.reload.messages.last.external_error).to eq('Error code: 30008')
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Twilio::IncomingMessageService
  include ::FileTypeHelper

  pattr_initialize [:params!]

  def perform
    return if twilio_channel.blank?

    set_contact
    set_conversation
    @message = @conversation.messages.create!(
      content: message_body,
      account_id: @inbox.account_id,
      inbox_id: @inbox.id,
      message_type: :incoming,
      sender: @contact,
      source_id: params[:SmsSid]
    )
    attach_files
  end

  private

  def twilio_channel
    @twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present?
    if params[:AccountSid].present? && params[:To].present?
      @twilio_channel ||= ::Channel::TwilioSms.find_by!(account_sid: params[:AccountSid],
                                                        phone_number: params[:To])
    end
    @twilio_channel
  end

  def inbox
    @inbox ||= twilio_channel.inbox
  end

  def account
    @account ||= inbox.account
  end

  def phone_number
    twilio_channel.sms? ? params[:From] : params[:From].gsub('whatsapp:', '')
  end

  def formatted_phone_number
    TelephoneNumber.parse(phone_number).international_number
  end

  def message_body
    params[:Body]&.delete("\u0000")
  end

  def set_contact
    contact_inbox = ::ContactInboxWithContactBuilder.new(
      source_id: params[:From],
      inbox: inbox,
      contact_attributes: contact_attributes
    ).perform

    @contact_inbox = contact_inbox
    @contact = contact_inbox.contact
  end

  def conversation_params
    {
      account_id: @inbox.account_id,
      inbox_id: @inbox.id,
      contact_id: @contact.id,
      contact_inbox_id: @contact_inbox.id,
      additional_attributes: additional_attributes
    }
  end

  def set_conversation
    # if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
    @conversation = if @inbox.lock_to_single_conversation
                      @contact_inbox.conversations.last
                    else
                      @contact_inbox.conversations.where
                                    .not(status: :resolved).last
                    end
    return if @conversation

    @conversation = ::Conversation.create!(conversation_params)
  end

  def contact_attributes
    {
      name: formatted_phone_number,
      phone_number: phone_number,
      additional_attributes: additional_attributes
    }
  end

  def additional_attributes
    if twilio_channel.sms?
      {
        from_zip_code: params[:FromZip],
        from_country: params[:FromCountry],
        from_state: params[:FromState]
      }
    else
      {}
    end
  end

  def attach_files
    return if params[:MediaUrl0].blank?

    attachment_file = Down.download(
      params[:MediaUrl0],
      # https://support.twilio.com/hc/en-us/articles/223183748-Protect-Media-Access-with-HTTP-Basic-Authentication-for-Programmable-Messaging
      http_basic_authentication: [twilio_channel.account_sid, twilio_channel.auth_token || twilio_channel.api_key_sid]
    )

    attachment = @message.attachments.new(
      account_id: @message.account_id,
      file_type: file_type(params[:MediaContentType0])
    )

    attachment.file.attach(
      io: attachment_file,
      filename: attachment_file.original_filename,
      content_type: attachment_file.content_type
    )

    @message.save!
  end
end

```

Response:
require 'rails_helper'

describe Twilio::IncomingMessageService do
  let!(:account) { create(:account) }
  let!(:twilio_channel) do
    create(:channel_twilio_sms, account: account, account_sid: 'ACxxx',
                                inbox: create(:inbox, account: account, greeting_enabled: false))
  end
  let!(:contact) { create(:contact, account: account, phone_number: '+12345') }
  let(:contact_inbox) { create(:contact_inbox, source_id: '+12345', contact: contact, inbox: twilio_channel.inbox) }
  let!(:conversation) { create(:conversation, contact: contact, inbox: twilio_channel.inbox, contact_inbox: contact_inbox) }

  describe '#perform' do
    it 'creates a new message in existing conversation' do
      params = {
        SmsSid: 'SMxx',
        From: '+12345',
        AccountSid: 'ACxxx',
        MessagingServiceSid: twilio_channel.messaging_service_sid,
        Body: 'testing3'
      }

      described_class.new(params: params).perform
      expect(conversation.reload.messages.last.content).to eq('testing3')
    end

    it 'removes null bytes' do
      params = {
        SmsSid: 'SMxx',
        From: '+12345',
        AccountSid: 'ACxxx',
        MessagingServiceSid: twilio_channel.messaging_service_sid,
        Body: "remove\u0000 null bytes\u0000"
      }

      described_class.new(params: params).perform
      expect(conversation.reload.messages.last.content).to eq('remove null bytes')
    end

    it 'wont throw error when the body is empty' do
      params = {
        SmsSid: 'SMxx',
        From: '+12345',
        AccountSid: 'ACxxx',
        MessagingServiceSid: twilio_channel.messaging_service_sid
      }

      described_class.new(params: params).perform
      expect(conversation.reload.messages.last.content).to be_nil
    end

    it 'creates a new conversation when payload is from different number' do
      params = {
        SmsSid: 'SMxx',
        From: '+123456',
        AccountSid: 'ACxxx',
        MessagingServiceSid: twilio_channel.messaging_service_sid,
        Body: 'new conversation'
      }

      described_class.new(params: params).perform
      expect(twilio_channel.inbox.conversations.count).to eq(2)
    end

    # Since we support the case with phone number as well. the previous case is with accoud_sid and messaging_service_sid
    context 'with a phone number' do
      let!(:twilio_channel) do
        create(:channel_twilio_sms, :with_phone_number, account: account, account_sid: 'ACxxx',
                                                        inbox: create(:inbox, account: account, greeting_enabled: false))
      end

      it 'creates a new message in existing conversation' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          To: twilio_channel.phone_number,
          Body: 'testing3'
        }

        described_class.new(params: params).perform
        expect(conversation.reload.messages.last.content).to eq('testing3')
      end

      it 'creates a new conversation when payload is from different number' do
        params = {
          SmsSid: 'SMxx',
          From: '+123456',
          AccountSid: 'ACxxx',
          To: twilio_channel.phone_number,
          Body: 'new conversation'
        }

        described_class.new(params: params).perform
        expect(twilio_channel.inbox.conversations.count).to eq(2)
      end

      it 'reopen last conversation if last conversation is resolved and lock to single conversation is enabled' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          To: twilio_channel.phone_number,
          Body: 'testing3'
        }

        twilio_channel.inbox.update(lock_to_single_conversation: true)
        conversation.update(status: 'resolved')
        described_class.new(params: params).perform
        # message appended to the last conversation
        expect(conversation.reload.messages.last.content).to eq('testing3')
        expect(conversation.reload.status).to eq('open')
      end

      it 'creates a new conversation if last conversation is resolved and lock to single conversation is disabled' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          To: twilio_channel.phone_number,
          Body: 'testing3'
        }

        twilio_channel.inbox.update(lock_to_single_conversation: false)
        conversation.update(status: 'resolved')
        described_class.new(params: params).perform
        expect(twilio_channel.inbox.conversations.count).to eq(2)
        expect(twilio_channel.inbox.conversations.last.messages.last.content).to eq('testing3')
      end

      it 'will not create a new conversation if last conversation is not resolved and lock to single conversation is disabled' do
        params = {
          SmsSid: 'SMxx',
          From: '+12345',
          AccountSid: 'ACxxx',
          To: twilio_channel.phone_number,
          Body: 'testing3'
        }

        twilio_channel.inbox.update(lock_to_single_conversation: false)
        conversation.update(status: Conversation.statuses.except('resolved').keys.sample)
        described_class.new(params: params).perform
        expect(twilio_channel.inbox.conversations.count).to eq(1)
        expect(twilio_channel.inbox.conversations.last.messages.last.content).to eq('testing3')
      end
    end

    context 'with multiple channels configured' do
      before do
        2.times.each do
          create(:channel_twilio_sms, :with_phone_number, account: account, account_sid: 'ACxxx', messaging_service_sid: nil,
                                                          inbox: create(:inbox, account: account, greeting_enabled: false))
        end
      end

      it 'creates a new conversation in appropriate channel' do
        twilio_sms_channel = create(:channel_twilio_sms, :with_phone_number, account: account, account_sid: 'ACxxx',
                                                                             inbox: create(:inbox, account: account, greeting_enabled: false))

        params = {
          SmsSid: 'SMxx',
          From: '+123456',
          AccountSid: 'ACxxx',
          To: twilio_sms_channel.phone_number,
          Body: 'new conversation'
        }

        described_class.new(params: params).perform
        expect(twilio_sms_channel.inbox.conversations.count).to eq(1)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Twilio::WebhookSetupService
  include Rails.application.routes.url_helpers

  pattr_initialize [:inbox!]

  def perform
    if channel.messaging_service_sid?
      update_messaging_service
    else
      update_phone_number
    end
  end

  private

  def update_messaging_service
    twilio_client
      .messaging.services(channel.messaging_service_sid)
      .update(
        inbound_method: 'POST',
        inbound_request_url: twilio_callback_index_url,
        use_inbound_webhook_on_number: false
      )
  end

  def update_phone_number
    if phone_numbers.empty?
      Rails.logger.warn "TWILIO_PHONE_NUMBER_NOT_FOUND: #{channel.phone_number}"
    else
      twilio_client
        .incoming_phone_numbers(phonenumber_sid)
        .update(sms_method: 'POST', sms_url: twilio_callback_index_url)
    end
  end

  def phonenumber_sid
    phone_numbers.first.sid
  end

  def phone_numbers
    @phone_numbers ||= twilio_client.incoming_phone_numbers.list(phone_number: channel.phone_number)
  end

  def channel
    @channel ||= inbox.channel
  end

  def twilio_client
    @twilio_client ||= ::Twilio::REST::Client.new(channel.account_sid, channel.auth_token)
  end
end

```

Response:
require 'rails_helper'

describe Twilio::WebhookSetupService do
  include Rails.application.routes.url_helpers

  let(:twilio_client) { instance_double(Twilio::REST::Client) }

  before do
    allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
  end

  describe '#perform' do
    context 'with a messaging service sid' do
      let(:channel_twilio_sms) { create(:channel_twilio_sms) }

      let(:messaging) { instance_double(Twilio::REST::Messaging) }
      let(:services) { instance_double(Twilio::REST::Messaging::V1::ServiceContext) }

      before do
        allow(twilio_client).to receive(:messaging).and_return(messaging)
        allow(messaging).to receive(:services).with(channel_twilio_sms.messaging_service_sid).and_return(services)
        allow(services).to receive(:update)
      end

      it 'updates the messaging service' do
        described_class.new(inbox: channel_twilio_sms.inbox).perform

        expect(services).to have_received(:update)
      end
    end

    context 'with a phone number' do
      let(:channel_twilio_sms) { create(:channel_twilio_sms, :with_phone_number) }

      let(:phone_double) { double }
      let(:phone_record_double) { double }

      before do
        allow(phone_double).to receive(:update)
        allow(phone_record_double).to receive(:sid).and_return('1234')
      end

      it 'logs error if phone_number is not found' do
        allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
        allow(phone_double).to receive(:list).and_return([])

        described_class.new(inbox: channel_twilio_sms.inbox).perform

        expect(phone_double).not_to have_received(:update)
      end

      it 'update webhook_url if phone_number is found' do
        allow(twilio_client).to receive(:incoming_phone_numbers).and_return(phone_double)
        allow(phone_double).to receive(:list).and_return([phone_record_double])

        described_class.new(inbox: channel_twilio_sms.inbox).perform

        expect(phone_double).to have_received(:update).with(
          sms_method: 'POST',
          sms_url: twilio_callback_index_url
        )
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Twilio::SendOnTwilioService < Base::SendOnChannelService
  private

  def channel_class
    Channel::TwilioSms
  end

  def perform_reply
    begin
      twilio_message = channel.send_message(**message_params)
    rescue Twilio::REST::TwilioError, Twilio::REST::RestError => e
      message.update!(status: :failed, external_error: e.message)
    end
    message.update!(source_id: twilio_message.sid) if twilio_message
  end

  def message_params
    {
      body: message.content,
      to: contact_inbox.source_id,
      media_url: attachments
    }
  end

  def attachments
    message.attachments.map(&:download_url)
  end

  def inbox
    @inbox ||= message.inbox
  end

  def channel
    @channel ||= inbox.channel
  end

  def outgoing_message?
    message.outgoing? || message.template?
  end
end

```

Response:
require 'rails_helper'

describe Twilio::SendOnTwilioService do
  subject(:outgoing_message_service) { described_class.new(message: message) }

  let(:twilio_client) { instance_double(Twilio::REST::Client) }
  let(:messages_double) { double }
  let(:message_record_double) { double }

  let!(:account) { create(:account) }
  let!(:widget_inbox) { create(:inbox, account: account) }
  let!(:twilio_sms) { create(:channel_twilio_sms, account: account) }
  let!(:twilio_whatsapp) { create(:channel_twilio_sms, medium: :whatsapp, account: account) }
  let!(:twilio_inbox) { create(:inbox, channel: twilio_sms, account: account) }
  let!(:twilio_whatsapp_inbox) { create(:inbox, channel: twilio_whatsapp, account: account) }
  let!(:contact) { create(:contact, account: account) }
  let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: twilio_inbox) }
  let(:conversation) { create(:conversation, contact: contact, inbox: twilio_inbox, contact_inbox: contact_inbox) }

  before do
    allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
    allow(twilio_client).to receive(:messages).and_return(messages_double)
  end

  describe '#perform' do
    context 'without reply' do
      it 'if message is private' do
        message = create(:message, message_type: 'outgoing', private: true, inbox: twilio_inbox, account: account)
        described_class.new(message: message).perform
        expect(twilio_client).not_to have_received(:messages)
        expect(message.reload.source_id).to be_nil
      end

      it 'if inbox channel is not twilio' do
        message = create(:message, message_type: 'outgoing', inbox: widget_inbox, account: account)
        expect { described_class.new(message: message).perform }.to raise_error 'Invalid channel service was called'
        expect(twilio_client).not_to have_received(:messages)
      end

      it 'if message is not outgoing' do
        message = create(:message, message_type: 'incoming', inbox: twilio_inbox, account: account)
        described_class.new(message: message).perform
        expect(twilio_client).not_to have_received(:messages)
        expect(message.reload.source_id).to be_nil
      end

      it 'if message has an source id' do
        message = create(:message, message_type: 'outgoing', inbox: twilio_inbox, account: account, source_id: SecureRandom.uuid)
        described_class.new(message: message).perform
        expect(twilio_client).not_to have_received(:messages)
      end
    end

    context 'with reply' do
      it 'if message is sent from chatwoot and is outgoing' do
        allow(messages_double).to receive(:create).and_return(message_record_double)
        allow(message_record_double).to receive(:sid).and_return('1234')

        outgoing_message = create(
          :message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation
        )
        described_class.new(message: outgoing_message).perform

        expect(outgoing_message.reload.source_id).to eq('1234')
      end
    end

    it 'if outgoing message has attachment and is for whatsapp' do
      # check for message attachment url
      allow(messages_double).to receive(:create).with(hash_including(media_url: [anything])).and_return(message_record_double)
      allow(message_record_double).to receive(:sid).and_return('1234')

      message = build(
        :message, message_type: 'outgoing', inbox: twilio_whatsapp_inbox, account: account, conversation: conversation
      )
      attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
      attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
      message.save!

      described_class.new(message: message).perform
      expect(messages_double).to have_received(:create).with(hash_including(media_url: [anything]))
    end

    it 'if outgoing message has attachment and is for sms' do
      # check for message attachment url
      allow(messages_double).to receive(:create).with(hash_including(media_url: [anything])).and_return(message_record_double)
      allow(message_record_double).to receive(:sid).and_return('1234')

      message = build(
        :message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation
      )
      attachment = message.attachments.new(account_id: message.account_id, file_type: :image)
      attachment.file.attach(io: Rails.root.join('spec/assets/avatar.png').open, filename: 'avatar.png', content_type: 'image/png')
      message.save!

      described_class.new(message: message).perform
      expect(messages_double).to have_received(:create).with(hash_including(media_url: [anything]))
    end

    it 'if message is sent from chatwoot fails' do
      allow(messages_double).to receive(:create).and_raise(Twilio::REST::TwilioError)

      outgoing_message = create(
        :message, message_type: 'outgoing', inbox: twilio_inbox, account: account, conversation: conversation
      )
      described_class.new(message: outgoing_message).perform
      expect(outgoing_message.reload.status).to eq('failed')
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Twilio::OneoffSmsCampaignService
  pattr_initialize [:campaign!]

  def perform
    raise "Invalid campaign #{campaign.id}" if campaign.inbox.inbox_type != 'Twilio SMS' || !campaign.one_off?
    raise 'Completed Campaign' if campaign.completed?

    # marks campaign completed so that other jobs won't pick it up
    campaign.completed!

    audience_label_ids = campaign.audience.select { |audience| audience['type'] == 'Label' }.pluck('id')
    audience_labels = campaign.account.labels.where(id: audience_label_ids).pluck(:title)
    process_audience(audience_labels)
  end

  private

  delegate :inbox, to: :campaign
  delegate :channel, to: :inbox

  def process_audience(audience_labels)
    campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
      next if contact.phone_number.blank?

      channel.send_message(to: contact.phone_number, body: campaign.message)
    end
  end
end

```

Response:
require 'rails_helper'

describe Twilio::OneoffSmsCampaignService do
  subject(:sms_campaign_service) { described_class.new(campaign: campaign) }

  let(:account) { create(:account) }
  let!(:twilio_sms) { create(:channel_twilio_sms) }
  let!(:twilio_inbox) { create(:inbox, channel: twilio_sms) }
  let(:label1) { create(:label, account: account) }
  let(:label2) { create(:label, account: account) }
  let!(:campaign) do
    create(:campaign, inbox: twilio_inbox, account: account,
                      audience: [{ type: 'Label', id: label1.id }, { type: 'Label', id: label2.id }])
  end
  let(:twilio_client) { double }
  let(:twilio_messages) { double }

  describe 'perform' do
    before do
      allow(Twilio::REST::Client).to receive(:new).and_return(twilio_client)
      allow(twilio_client).to receive(:messages).and_return(twilio_messages)
    end

    it 'raises error if the campaign is completed' do
      campaign.completed!

      expect { sms_campaign_service.perform }.to raise_error 'Completed Campaign'
    end

    it 'raises error invalid campaign when its not a oneoff sms campaign' do
      campaign = create(:campaign)

      expect { described_class.new(campaign: campaign).perform }.to raise_error "Invalid campaign #{campaign.id}"
    end

    it 'send messages to contacts in the audience and marks the campaign completed' do
      contact_with_label1, contact_with_label2, contact_with_both_labels = FactoryBot.create_list(:contact, 3, :with_phone_number, account: account)
      contact_with_label1.update_labels([label1.title])
      contact_with_label2.update_labels([label2.title])
      contact_with_both_labels.update_labels([label1.title, label2.title])
      expect(twilio_messages).to receive(:create).with(
        body: campaign.message,
        messaging_service_sid: twilio_sms.messaging_service_sid,
        to: contact_with_label1.phone_number,
        status_callback: 'http://localhost:3000/twilio/delivery_status'
      ).once
      expect(twilio_messages).to receive(:create).with(
        body: campaign.message,
        messaging_service_sid: twilio_sms.messaging_service_sid,
        to: contact_with_label2.phone_number,
        status_callback: 'http://localhost:3000/twilio/delivery_status'
      ).once
      expect(twilio_messages).to receive(:create).with(
        body: campaign.message,
        messaging_service_sid: twilio_sms.messaging_service_sid,
        to: contact_with_both_labels.phone_number,
        status_callback: 'http://localhost:3000/twilio/delivery_status'
      ).once

      sms_campaign_service.perform
      expect(campaign.reload.completed?).to be true
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class PageCrawlerService
  attr_reader :external_link

  def initialize(external_link)
    @external_link = external_link
    @doc = Nokogiri::HTML(HTTParty.get(external_link).body)
  end

  def page_links
    sitemap? ? extract_links_from_sitemap : extract_links_from_html
  end

  def page_title
    title_element = @doc.at_xpath('//title')
    title_element&.text&.strip
  end

  def body_text_content
    ReverseMarkdown.convert @doc.at_xpath('//body'), unknown_tags: :bypass, github_flavored: true
  end

  private

  def sitemap?
    @external_link.end_with?('.xml')
  end

  def extract_links_from_sitemap
    @doc.xpath('//loc').to_set(&:text)
  end

  def extract_links_from_html
    @doc.xpath('//a/@href').to_set do |link|
      absolute_url = URI.join(@external_link, link.value).to_s
      absolute_url
    end
  end
end

```

Response:
require 'rails_helper'

describe PageCrawlerService do
  let(:html_link) { 'http://test.com' }
  let(:sitemap_link) { 'http://test.com/sitemap.xml' }
  let(:service_html) { described_class.new(html_link) }
  let(:service_sitemap) { described_class.new(sitemap_link) }

  let(:html_body) do
    <<-HTML
      <html>
        <head><title>Test Title</title></head>
        <body><a href="link1">Link 1</a><a href="link2">Link 2</a></body>
      </html>
    HTML
  end

  let(:sitemap_body) do
    <<-XML
      <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
        <url><loc>http://test.com/link1</loc></url>
        <url><loc>http://test.com/link2</loc></url>
      </urlset>
    XML
  end

  before do
    stub_request(:get, html_link).to_return(body: html_body, status: 200)
    stub_request(:get, sitemap_link).to_return(body: sitemap_body, status: 200)
  end

  describe '#page_links' do
    context 'when a HTML page is given' do
      it 'returns all links on the page' do
        expect(service_html.page_links).to eq(Set.new(['http://test.com/link1', 'http://test.com/link2']))
      end
    end

    context 'when a sitemap is given' do
      it 'returns all links in the sitemap' do
        expect(service_sitemap.page_links).to eq(Set.new(['http://test.com/link1', 'http://test.com/link2']))
      end
    end
  end

  describe '#page_title' do
    it 'returns the title of the page' do
      expect(service_html.page_title).to eq('Test Title')
    end
  end

  describe '#body_text_content' do
    it 'returns the markdown converted body content of the page' do
      expect(service_html.body_text_content.strip).to eq('[Link 1](link1)[Link 2](link2)')
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Enterprise::MessageTemplates::ResponseBotService
  pattr_initialize [:conversation!]

  def perform
    ActiveRecord::Base.transaction do
      @response = get_response(conversation.messages.incoming.last.content)
      process_response
    end
  rescue StandardError => e
    process_action('handoff') # something went wrong, pass to agent
    ChatwootExceptionTracker.new(e, account: conversation.account).capture_exception
    true
  end

  def response_sections(content)
    sections = ''

    inbox.get_responses(content).each do |response|
      sections += "{context_id: #{response.id}, context: #{response.question} ? #{response.answer}},"
    end
    sections
  end

  private

  delegate :contact, :account, :inbox, to: :conversation

  def get_response(content)
    previous_messages = []
    get_previous_messages(previous_messages)
    ChatGpt.new(response_sections(content)).generate_response('', previous_messages)
  end

  def get_previous_messages(previous_messages)
    conversation.messages.where(message_type: [:outgoing, :incoming]).where(private: false).find_each do |message|
      next if message.content_type != 'text'

      role = determine_role(message)
      previous_messages << { content: message.content, role: role }
    end
  end

  def determine_role(message)
    message.message_type == 'incoming' ? 'user' : 'system'
  end

  def process_response
    if @response['response'] == 'conversation_handoff'
      process_action('handoff')
    else
      create_messages
    end
  end

  def process_action(action)
    case action
    when 'handoff'
      conversation.messages.create!('message_type': :outgoing, 'account_id': conversation.account_id, 'inbox_id': conversation.inbox_id,
                                    'content': 'passing to an agent')
      conversation.update(status: :open)
    end
  end

  def create_messages
    message_content = @response['response']
    message_content += generate_sources_section if @response['context_ids'].present?

    create_outgoing_message(message_content)
  end

  def generate_sources_section
    article_ids = @response['context_ids']
    sources_content = ''

    articles_hash = get_article_hash(article_ids.uniq)

    articles_hash.first(3).each do |article_hash|
      sources_content += " - [#{article_hash[:response].question}](#{article_hash[:response_document].document_link}) \n"
    end
    sources_content = "\n \n \n **Sources**  \n#{sources_content}" if sources_content.present?
    sources_content
  end

  def create_outgoing_message(message_content)
    conversation.messages.create!(
      {
        message_type: :outgoing,
        account_id: conversation.account_id,
        inbox_id: conversation.inbox_id,
        content: message_content
      }
    )
  end

  def get_article_hash(article_ids)
    seen_documents = Set.new
    article_ids.uniq.filter_map do |article_id|
      response = Response.find(article_id)
      response_document = response.response_document
      next if response_document.blank? || seen_documents.include?(response_document)

      seen_documents << response_document
      { response: response, response_document: response_document }
    end
  end
end

```

Response:
require 'rails_helper'

RSpec.describe Enterprise::MessageTemplates::ResponseBotService, type: :service do
  let!(:conversation) { create(:conversation, status: :pending) }
  let(:service) { described_class.new(conversation: conversation) }
  let(:chat_gpt_double) { instance_double(ChatGpt) }
  let(:response_source) { create(:response_source, account: conversation.account) }
  let(:response_object) { instance_double(Response, id: 1, question: 'Q1', answer: 'A1') }

  before do
    # Uncomment if you want to run the spec in your local machine
    # Features::ResponseBotService.new.enable_in_installation
    skip('Skipping since vector is not enabled in this environment') unless Features::ResponseBotService.new.vector_extension_enabled?
    stub_request(:post, 'https://api.openai.com/v1/embeddings').to_return(status: 200, body: {}.to_json,
                                                                          headers: { Content_Type: 'application/json' })
    create(:message, message_type: :incoming, conversation: conversation, content: 'Hi')
    create(:message, message_type: :outgoing, conversation: conversation, content: 'Hello')
    4.times { create(:response, response_source: response_source) }
    allow(ChatGpt).to receive(:new).and_return(chat_gpt_double)
    allow(chat_gpt_double).to receive(:generate_response).and_return({ 'response' => 'some_response', 'context_ids' => Response.all.map(&:id) })
    allow(conversation.inbox).to receive(:get_responses).with('Hi').and_return([response_object])
  end

  describe '#perform' do
    context 'when successful' do
      it 'creates an outgoing message along with article references' do
        expect do
          service.perform
        end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)

        last_message = conversation.messages.last
        expect(last_message.content).to include('some_response')
        expect(last_message.content).to include(Response.first.question)
        expect(last_message.content).to include('**Sources**')
      end
    end

    context 'when context_ids are not present' do
      it 'creates an outgoing message without article references' do
        allow(chat_gpt_double).to receive(:generate_response).and_return({ 'response' => 'some_response' })

        expect do
          service.perform
        end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)

        last_message = conversation.messages.last
        expect(last_message.content).to include('some_response')
        expect(last_message.content).not_to include('**Sources**')
      end
    end

    context 'when response doesnt have response document' do
      it 'creates an outgoing message without article references' do
        response = create(:response, response_source: response_source, response_document: nil)
        allow(chat_gpt_double).to receive(:generate_response).and_return({ 'response' => 'some_response', 'context_ids' => [response.id] })

        expect do
          service.perform
        end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)

        last_message = conversation.messages.last
        expect(last_message.content).to include('some_response')
        expect(last_message.content).not_to include('**Sources**')
      end
    end

    context 'when JSON::ParserError is raised' do
      it 'creates a handoff message' do
        allow(chat_gpt_double).to receive(:generate_response).and_raise(JSON::ParserError)

        expect do
          service.perform
        end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)

        expect(conversation.messages.last.content).to eq('passing to an agent')
        expect(conversation.status).to eq('open')
      end
    end

    context 'when StandardError is raised' do
      it 'captures the exception' do
        allow(chat_gpt_double).to receive(:generate_response).and_raise(StandardError)

        expect(ChatwootExceptionTracker).to receive(:new).and_call_original

        expect do
          service.perform
        end.to change { conversation.messages.where(message_type: :outgoing).count }.by(1)

        expect(conversation.messages.last.content).to eq('passing to an agent')
        expect(conversation.status).to eq('open')
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Enterprise::Billing::CreateSessionService
  def create_session(customer_id, return_url = ENV.fetch('FRONTEND_URL'))
    Stripe::BillingPortal::Session.create(
      {
        customer: customer_id,
        return_url: return_url
      }
    )
  end
end

```

Response:
require 'rails_helper'

describe Enterprise::Billing::CreateSessionService do
  subject(:create_session_service) { described_class }

  describe '#perform' do
    it 'calls stripe billing portal session' do
      customer_id = 'cus_random_number'
      return_url = 'https://www.chatwoot.com'
      allow(Stripe::BillingPortal::Session).to receive(:create).with({ customer: customer_id, return_url: return_url })

      create_session_service.new.create_session(customer_id, return_url)

      expect(Stripe::BillingPortal::Session).to have_received(:create).with(
        {
          customer: customer_id,
          return_url: return_url
        }
      )
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Enterprise::Billing::CreateStripeCustomerService
  pattr_initialize [:account!]

  DEFAULT_QUANTITY = 2

  def perform
    customer_id = prepare_customer_id
    subscription = Stripe::Subscription.create(
      {
        customer: customer_id,
        items: [{ price: price_id, quantity: default_quantity }]
      }
    )
    account.update!(
      custom_attributes: {
        stripe_customer_id: customer_id,
        stripe_price_id: subscription['plan']['id'],
        stripe_product_id: subscription['plan']['product'],
        plan_name: default_plan['name'],
        subscribed_quantity: subscription['quantity']
      }
    )
  end

  private

  def prepare_customer_id
    customer_id = account.custom_attributes['stripe_customer_id']
    if customer_id.blank?
      customer = Stripe::Customer.create({ name: account.name, email: billing_email })
      customer_id = customer.id
    end
    customer_id
  end

  def default_quantity
    default_plan['default_quantity'] || DEFAULT_QUANTITY
  end

  def billing_email
    account.administrators.first.email
  end

  def default_plan
    installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
    @default_plan ||= installation_config.value.first
  end

  def price_id
    price_ids = default_plan['price_ids']
    price_ids.first
  end
end

```

Response:
require 'rails_helper'

describe Enterprise::Billing::CreateStripeCustomerService do
  subject(:create_stripe_customer_service) { described_class }

  let(:account) { create(:account) }
  let!(:admin1) { create(:user, account: account, role: :administrator) }
  let(:admin2) { create(:user, account: account, role: :administrator) }

  describe '#perform' do
    before do
      create(
        :installation_config,
        { name: 'CHATWOOT_CLOUD_PLANS', value: [
          { 'name' => 'A Plan Name', 'product_id' => ['prod_hacker_random'], 'price_ids' => ['price_hacker_random'] }
        ] }
      )
    end

    it 'does not call stripe methods if customer id is present' do
      account.update!(custom_attributes: { stripe_customer_id: 'cus_random_number' })

      allow(Stripe::Customer).to receive(:create)
      allow(Stripe::Subscription).to receive(:create)
        .and_return(
          {
            plan: { id: 'price_random_number', product: 'prod_random_number' },
            quantity: 2
          }.with_indifferent_access
        )

      create_stripe_customer_service.new(account: account).perform

      expect(Stripe::Customer).not_to have_received(:create)
      expect(Stripe::Subscription)
        .to have_received(:create)
        .with({ customer: 'cus_random_number', items: [{ price: 'price_hacker_random', quantity: 2 }] })

      expect(account.reload.custom_attributes).to eq(
        {
          stripe_customer_id: 'cus_random_number',
          stripe_price_id: 'price_random_number',
          stripe_product_id: 'prod_random_number',
          subscribed_quantity: 2,
          plan_name: 'A Plan Name'
        }.with_indifferent_access
      )
    end

    it 'calls stripe methods to create a customer and updates the account' do
      customer = double
      allow(Stripe::Customer).to receive(:create).and_return(customer)
      allow(customer).to receive(:id).and_return('cus_random_number')
      allow(Stripe::Subscription)
        .to receive(:create)
        .and_return(
          {
            plan: { id: 'price_random_number', product: 'prod_random_number' },
            quantity: 2
          }.with_indifferent_access
        )

      create_stripe_customer_service.new(account: account).perform

      expect(Stripe::Customer).to have_received(:create).with({ name: account.name, email: admin1.email })
      expect(Stripe::Subscription)
        .to have_received(:create)
        .with({ customer: customer.id, items: [{ price: 'price_hacker_random', quantity: 2 }] })

      expect(account.reload.custom_attributes).to eq(
        {
          stripe_customer_id: customer.id,
          stripe_price_id: 'price_random_number',
          stripe_product_id: 'prod_random_number',
          subscribed_quantity: 2,
          plan_name: 'A Plan Name'
        }.with_indifferent_access
      )
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
class Enterprise::Billing::HandleStripeEventService
  def perform(event:)
    ensure_event_context(event)
    case @event.type
    when 'customer.subscription.updated'
      process_subscription_updated
    when 'customer.subscription.deleted'
      process_subscription_deleted
    else
      Rails.logger.debug { "Unhandled event type: #{event.type}" }
    end
  end

  private

  def process_subscription_updated
    plan = find_plan(subscription['plan']['product'])
    # skipping self hosted plan events
    return if plan.blank? || account.blank?

    update_account_attributes(subscription, plan)

    change_plan_features
  end

  def update_account_attributes(subscription, plan)
    # https://stripe.com/docs/api/subscriptions/object

    account.update(
      custom_attributes: {
        stripe_customer_id: subscription.customer,
        stripe_price_id: subscription['plan']['id'],
        stripe_product_id: subscription['plan']['product'],
        plan_name: plan['name'],
        subscribed_quantity: subscription['quantity'],
        subscription_status: subscription['status'],
        subscription_ends_on: Time.zone.at(subscription['current_period_end'])
      }
    )
  end

  def process_subscription_deleted
    # skipping self hosted plan events
    return if account.blank?

    Enterprise::Billing::CreateStripeCustomerService.new(account: account).perform
  end

  def change_plan_features
    if default_plan?
      account.disable_features(*features_to_update)
    else
      account.enable_features(*features_to_update)
    end
    account.save!
  end

  def ensure_event_context(event)
    @event = event
  end

  def features_to_update
    %w[help_center campaigns team_management channel_twitter channel_facebook channel_email]
  end

  def subscription
    @subscription ||= @event.data.object
  end

  def account
    @account ||= Account.where("custom_attributes->>'stripe_customer_id' = ?", subscription.customer).first
  end

  def find_plan(plan_id)
    installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
    installation_config.value.find { |config| config['product_id'].include?(plan_id) }
  end

  def default_plan?
    installation_config = InstallationConfig.find_by(name: 'CHATWOOT_CLOUD_PLANS')
    default_plan = installation_config.value.first
    @account.custom_attributes['plan_name'] == default_plan['name']
  end
end

```

Response:
require 'rails_helper'

describe Enterprise::Billing::HandleStripeEventService do
  subject(:stripe_event_service) { described_class }

  let(:event) { double }
  let(:data) { double }
  let(:subscription) { double }
  let!(:account) { create(:account, custom_attributes: { stripe_customer_id: 'cus_123' }) }

  before do
    allow(event).to receive(:data).and_return(data)
    allow(data).to receive(:object).and_return(subscription)
    allow(subscription).to receive(:[]).with('plan')
                                       .and_return({
                                                     'id' => 'test', 'product' => 'plan_id', 'name' => 'plan_name'
                                                   })
    allow(subscription).to receive(:[]).with('quantity').and_return('10')
    allow(subscription).to receive(:[]).with('status').and_return('active')
    allow(subscription).to receive(:[]).with('current_period_end').and_return(1_686_567_520)
    allow(subscription).to receive(:customer).and_return('cus_123')
    create(:installation_config, {
             name: 'CHATWOOT_CLOUD_PLANS',
             value: [
               {
                 'name' => 'Hacker',
                 'product_id' => ['plan_id'],
                 'price_ids' => ['price_1']
               },
               {
                 'name' => 'Startups',
                 'product_id' => ['plan_id_2'],
                 'price_ids' => ['price_2']
               }
             ]
           })
  end

  describe '#perform' do
    it 'handle customer.subscription.updated' do
      allow(event).to receive(:type).and_return('customer.subscription.updated')
      allow(subscription).to receive(:customer).and_return('cus_123')
      stripe_event_service.new.perform(event: event)
      expect(account.reload.custom_attributes).to eq({
                                                       'stripe_customer_id' => 'cus_123',
                                                       'stripe_price_id' => 'test',
                                                       'stripe_product_id' => 'plan_id',
                                                       'plan_name' => 'Hacker',
                                                       'subscribed_quantity' => '10',
                                                       'subscription_ends_on' => Time.zone.at(1_686_567_520).as_json,
                                                       'subscription_status' => 'active'
                                                     })
    end

    it 'disable features on customer.subscription.updated for default plan' do
      allow(event).to receive(:type).and_return('customer.subscription.updated')
      allow(subscription).to receive(:customer).and_return('cus_123')
      stripe_event_service.new.perform(event: event)
      expect(account.reload.custom_attributes).to eq({
                                                       'stripe_customer_id' => 'cus_123',
                                                       'stripe_price_id' => 'test',
                                                       'stripe_product_id' => 'plan_id',
                                                       'plan_name' => 'Hacker',
                                                       'subscribed_quantity' => '10',
                                                       'subscription_ends_on' => Time.zone.at(1_686_567_520).as_json,
                                                       'subscription_status' => 'active'
                                                     })
      expect(account).not_to be_feature_enabled('channel_email')
      expect(account).not_to be_feature_enabled('help_center')
    end

    it 'handles customer.subscription.deleted' do
      stripe_customer_service = double
      allow(event).to receive(:type).and_return('customer.subscription.deleted')
      allow(Enterprise::Billing::CreateStripeCustomerService).to receive(:new).and_return(stripe_customer_service)
      allow(stripe_customer_service).to receive(:perform)
      stripe_event_service.new.perform(event: event)
      expect(Enterprise::Billing::CreateStripeCustomerService).to have_received(:new).with(account: account)
    end
  end

  describe '#perform for Startups plan' do
    before do
      allow(event).to receive(:data).and_return(data)
      allow(data).to receive(:object).and_return(subscription)
      allow(subscription).to receive(:[]).with('plan')
                                         .and_return({
                                                       'id' => 'test', 'product' => 'plan_id_2', 'name' => 'plan_name'
                                                     })
      allow(subscription).to receive(:[]).with('quantity').and_return('10')
      allow(subscription).to receive(:customer).and_return('cus_123')
    end

    it 'enable features on customer.subscription.updated' do
      allow(event).to receive(:type).and_return('customer.subscription.updated')
      allow(subscription).to receive(:customer).and_return('cus_123')
      stripe_event_service.new.perform(event: event)
      expect(account.reload.custom_attributes).to eq({
                                                       'stripe_customer_id' => 'cus_123',
                                                       'stripe_price_id' => 'test',
                                                       'stripe_product_id' => 'plan_id_2',
                                                       'plan_name' => 'Startups',
                                                       'subscribed_quantity' => '10',
                                                       'subscription_ends_on' => Time.zone.at(1_686_567_520).as_json,
                                                       'subscription_status' => 'active'
                                                     })
      expect(account).to be_feature_enabled('channel_email')
      expect(account).to be_feature_enabled('help_center')
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class UpdateQueryFromParamsService
  def initialize(query, user)
    self.query = query
    self.current_user = user
  end

  # rubocop:disable Metrics/AbcSize
  def call(params, valid_subset: false)
    apply_group_by(params)

    apply_sort_by(params)

    apply_filters(params)

    apply_columns(params)

    apply_sums(params)

    apply_timeline(params)

    apply_hierarchy(params)

    apply_highlighting(params)

    apply_display_representation(params)

    apply_include_subprojects(params)

    apply_timestamps(params)

    disable_hierarchy_when_only_grouped_by(params)

    if valid_subset
      query.valid_subset!
    end

    if query.valid?
      ServiceResult.success(result: query)
    else
      ServiceResult.failure(errors: query.errors)
    end
  end
  # rubocop:enable Metrics/AbcSize

  private

  def apply_group_by(params)
    query.group_by = params[:group_by] if params.key?(:group_by)
  end

  def apply_sort_by(params)
    query.sort_criteria = params[:sort_by] if params[:sort_by]
  end

  def apply_filters(params)
    return unless params[:filters]

    query.filters = []

    params[:filters].each do |filter|
      query.add_filter(filter[:field], filter[:operator], filter[:values])
    end
  end

  def apply_columns(params)
    query.column_names = params[:columns] if params[:columns]
  end

  def apply_sums(params)
    query.display_sums = params[:display_sums] if params.key?(:display_sums)
  end

  def apply_timeline(params)
    query.timeline_visible = params[:timeline_visible] if params.key?(:timeline_visible)
    query.timeline_zoom_level = params[:timeline_zoom_level] if params.key?(:timeline_zoom_level)
    query.timeline_labels = params[:timeline_labels] if params.key?(:timeline_labels)
  end

  def apply_hierarchy(params)
    query.show_hierarchies = params[:show_hierarchies] if params.key?(:show_hierarchies)
  end

  def apply_highlighting(params)
    query.highlighting_mode = params[:highlighting_mode] if params.key?(:highlighting_mode)
    query.highlighted_attributes = params[:highlighted_attributes] if params.key?(:highlighted_attributes)
  end

  def apply_display_representation(params)
    query.display_representation = params[:display_representation] if params.key?(:display_representation)
  end

  def apply_include_subprojects(params)
    query.include_subprojects = params[:include_subprojects] if params.key?(:include_subprojects)
  end

  def apply_timestamps(params)
    query.timestamps = params[:timestamps] if params.key?(:timestamps)
  end

  def disable_hierarchy_when_only_grouped_by(params)
    if params.key?(:group_by) && !params.key?(:show_hierarchies)
      query.show_hierarchies = false
    end
  end

  attr_accessor :query,
                :current_user,
                :params
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe UpdateQueryFromParamsService,
               type: :model do
  let(:user) { build_stubbed(:user) }
  let(:query) { build_stubbed(:query) }

  let(:instance) { described_class.new(query, user) }

  let(:params) { {} }

  describe '#call' do
    subject { instance.call(params) }

    context 'group_by' do
      context 'for an existing value' do
        let(:params) { { group_by: 'status' } }

        it 'sets the value' do
          subject

          expect(query.group_by)
            .to eql('status')
        end
      end

      context 'for an explicitly nil value' do
        let(:params) { { group_by: nil } }

        it 'sets the value' do
          subject

          expect(query.group_by)
            .to be_nil
        end
      end
    end

    context 'filters' do
      let(:params) do
        { filters: [{ field: 'status_id', operator: '=', values: ['1', '2'] }] }
      end

      context 'for a valid filter' do
        it 'sets the filter' do
          subject

          expect(query.filters.length)
            .to be(1)
          expect(query.filters[0].name)
            .to be(:status_id)
          expect(query.filters[0].operator)
            .to eql('=')
          expect(query.filters[0].values)
            .to eql(['1', '2'])
        end
      end
    end

    context 'sort_by' do
      let(:params) do
        { sort_by: [['status_id', 'desc']] }
      end

      it 'sets the order' do
        subject

        expect(query.sort_criteria)
          .to eql([['status_id', 'desc']])
      end
    end

    context 'columns' do
      let(:params) do
        { columns: ['assigned_to', 'author', 'category', 'subject'] }
      end

      it 'sets the columns' do
        subject

        expect(query.column_names)
          .to match_array(params[:columns].map(&:to_sym))
      end
    end

    context 'display representation' do
      let(:params) do
        { display_representation: 'list' }
      end

      it 'sets the display_representation' do
        subject

        expect(query.display_representation)
          .to eq('list')
      end
    end

    context 'highlighting mode', with_ee: %i[conditional_highlighting] do
      let(:params) do
        { highlighting_mode: 'status' }
      end

      it 'sets the highlighting_mode' do
        subject

        expect(query.highlighting_mode)
          .to eq(:status)
      end
    end

    context 'default highlighting mode', with_ee: %i[conditional_highlighting] do
      let(:params) do
        {}
      end

      it 'sets the highlighting_mode' do
        subject

        expect(query.highlighting_mode)
          .to eq(:inline)
      end
    end

    context 'highlighting mode without EE' do
      let(:params) do
        { highlighting_mode: 'status' }
      end

      it 'sets the highlighting_mode' do
        subject

        expect(query.highlighting_mode)
          .to eq(:none)
      end
    end

    context 'when using include subprojects' do
      let(:params) do
        { include_subprojects: }
      end

      context 'when true' do
        let(:include_subprojects) { true }

        it 'sets the display_representation' do
          subject

          expect(query.include_subprojects)
            .to be true
        end
      end

      context 'when false' do
        let(:include_subprojects) { false }

        it 'sets the display_representation' do
          subject

          expect(query.include_subprojects)
            .to be false
        end
      end
    end

    context "when providing timestamps" do
      let(:timestamps) do
        [
          Timestamp.parse("2022-10-29T23:01:23Z"),
          Timestamp.parse("oneWeekAgo@12:00+00:00"),
          Timestamp.parse("PT0S")
        ]
      end
      let(:params) { { timestamps: } }

      it 'sets the timestamps' do
        subject

        expect(query.timestamps).to eq timestamps
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

class UpdateProjectsTypesService < BaseProjectService
  def call(type_ids)
    type_ids = standard_types if type_ids.blank?

    if types_missing?(type_ids)
      project.errors.add(:types,
                         :in_use_by_work_packages,
                         types: missing_types(type_ids).map(&:name).join(', '))
      false
    else
      update_project_types(type_ids)

      true
    end
  end

  protected

  def standard_types
    type = ::Type.standard_type
    if type.nil?
      []
    else
      [type.id]
    end
  end

  def types_missing?(type_ids)
    !missing_types(type_ids).empty?
  end

  def missing_types(type_ids)
    types_used_by_work_packages.select { |t| type_ids.exclude?(t.id) }
  end

  def types_used_by_work_packages
    @types_used_by_work_packages ||= project.types_used_by_work_packages
  end

  def update_project_types(type_ids)
    new_types_to_add = type_ids - project.type_ids
    project.type_ids = type_ids
    project.work_package_custom_field_ids |= WorkPackageCustomField.joins(:types).where(types: { id: new_types_to_add }).ids
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.

require 'spec_helper'

RSpec.describe UpdateProjectsTypesService do
  let(:project) { instance_double(Project, types_used_by_work_packages: []) }
  let(:standard_type) { build_stubbed(:type_standard) }

  subject(:instance) { described_class.new(project) }

  before do
    allow(Type).to receive(:standard_type).and_return standard_type
  end

  describe '.call' do
    subject { instance.call(ids) }

    before do
      allow(project).to receive(:type_ids=)
    end

    shared_examples 'activating custom fields' do
      let(:project) { create(:project, no_types: true) }
      let!(:custom_field) { create(:text_wp_custom_field, types:) }

      it 'updates the active custom fields' do
        expect { subject }
          .to change { project.reload.work_package_custom_field_ids }
          .from([])
          .to([custom_field.id])
      end

      it 'does not activates the same custom field twice' do
        expect { subject }.to change { project.reload.work_package_custom_field_ids }
        expect { subject }.not_to change { project.reload.work_package_custom_field_ids }
      end

      context 'for a project with already existing types' do
        let(:project) { create(:project, types:, work_package_custom_fields: [create(:text_wp_custom_field)]) }

        it 'does not change custom fields' do
          expect { subject }.not_to change { project.reload.work_package_custom_field_ids }
        end
      end
    end

    context 'with ids provided' do
      let(:ids) { [1, 2, 3] }

      it 'returns true and updates the ids' do
        expect(subject).to be_truthy
        expect(project).to have_received(:type_ids=).with(ids)
      end

      include_examples 'activating custom fields' do
        let(:types) { create_list(:type, 2) }
        let(:ids) { types.collect(&:id) }
      end
    end

    context 'with no id passed' do
      let(:ids) { [] }

      it 'adds the id of the default type and returns true' do
        expect(subject).to be_truthy
        expect(project).to have_received(:type_ids=).with([standard_type.id])
      end

      include_examples 'activating custom fields' do
        let(:standard_type) { create(:type_standard) }
        let(:types) { [standard_type] }
      end
    end

    context 'with nil passed' do
      let(:ids) { nil }

      it 'adds the id of the default type and returns true' do
        expect(subject).to be_truthy
        expect(project).to have_received(:type_ids=).with([standard_type.id])
      end

      include_examples 'activating custom fields' do
        let(:standard_type) { create(:type_standard) }
        let(:types) { [standard_type] }
      end
    end

    context 'when the id of a type in use is not provided' do
      let(:type) { build_stubbed(:type) }
      let(:ids) { [1] }

      before do
        allow(project).to receive(:types_used_by_work_packages).and_return([type])
        allow(project).to receive(:work_package_custom_field_ids=).and_return([type])
      end

      it 'returns false and sets an error message' do
        errors = instance_double(ActiveModel::Errors)
        allow(errors).to receive(:add)
        allow(project).to receive(:errors).and_return(errors)

        expect(subject).to be_falsey
        expect(errors).to have_received(:add).with(:types, :in_use_by_work_packages, types: type.name)
        expect(project).not_to have_received(:type_ids=)
        expect(project).not_to have_received(:work_package_custom_field_ids=)
      end
    end
  end
end


Instruction:
Write RSpec test file for following ruby class
```ruby
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Authorization
  module_function

  # Returns all users having a certain permission within a project
  def users(action, project)
    Authorization::UserAllowedQuery.query(action, project)
  end

  # Returns all projects a user has a certain permission in
  def projects(action, user)
    Project.allowed_to(user, action)
  end

  # Returns all work packages a user has a certain permission in (or in the project it belongs to)
  def work_packages(action, user)
    WorkPackage.allowed_to(user, action)
  end

  # Returns all roles a user has in a certain project, for a specific entity or globally
  def roles(user, context = nil)
    if context.is_a?(Project)
      Authorization::UserProjectRolesQuery.query(user, context)
    elsif Member.can_be_member_of?(context)
      Authorization::UserEntityRolesQuery.query(user, context)
    else
      Authorization::UserGlobalRolesQuery.query(user)
    end
  end

  # Normalizes the different types of permission arguments into Permission objects.
  # Possible arguments
  #  - Symbol permission names (e.g. :view_work_packages)
  #  - Hash with :controller and :action (e.g. { controller: 'work_packages', action: 'show' })
  #
  # Exceptions
  #  - When there is no permission matching the +action+ parameter, either an empty array is returned
  #    or an +UnknownPermissionError+ is raised (depending on the +raise_on_unknown+ parameter).
  # .  Additionally a debugger message is logged.
  #    If the permission is disabled, it will not raise an error or log debug output.
  def permissions_for(action, raise_on_unknown: false) # rubocop:disable Metrics/PerceivedComplexity, Metrics/AbcSize
    return [action] if action.is_a?(OpenProject::AccessControl::Permission)
    return action if action.is_a?(Array) && (action.empty? || action.all?(OpenProject::AccessControl::Permission))

    perms = if action.is_a?(Hash)
              OpenProject::AccessControl.allow_actions(action)
            else
              [OpenProject::AccessControl.permission(action)].compact
            end

    if perms.blank?
      if !OpenProject::AccessControl.disabled_permission?(action)
        Rails.logger.debug { "Used permission \"#{action}\" that is not defined. It will never return true." }
        raise UnknownPermissionError.new(action) if raise_on_unknown
      end

      return []
    end

    perms
  end

  # Returns a set of normalized permissions filtered for a given context
  #  - When there are no permissions available for the given context (based on +permissible_on+
  #    attribute of the permission), an +IllegalPermissionContextError+ is raised
  def contextual_permissions(action, context, raise_on_unknown: false)
    perms = permissions_for(action, raise_on_unknown:)
    return [] if perms.blank?

    context_perms = perms.select { |p| p.permissible_on?(context) }
    raise IllegalPermissionContextError.new(action, perms, context) if context_perms.blank?

    context_perms
  end
end

```

Response:
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2023 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

require 'spec_helper'

RSpec.describe Authorization do
  let(:user) { build_stubbed(:user) }
  let(:action) { :view_work_packages }

  describe '.users' do
    it 'calls Authorization::UserAllowedQuery' do
      expect(Authorization::UserAllowedQuery).to receive(:query).with(action, user)
      described_class.users(action, user)
    end
  end

  describe '.projects' do
    it 'uses the .allowed_to scope on Project' do
      expect(Project).to receive(:allowed_to).with(user, action)
      described_class.projects(action, user)
    end
  end

  describe '.work_packages' do
    it 'uses the .allowed_to scope on WorkPackage' do
      expect(WorkPackage).to receive(:allowed_to).with(user, action)
      described_class.work_packages(action, user)
    end
  end

  describe '.roles' do
    context 'with a project' do
      let(:context) { build_stubbed(:project) }

      it 'calls Authorization::UserProjectRolesQuery' do
        expect(Authorization::UserProjectRolesQuery).to receive(:query).with(user, context)
        described_class.roles(user, context)
      end
    end

    context 'with a WorkPackage' do
      let(:context) { build_stubbed(:work_package) }

      it 'calls Authorization::UserEntityRolesQuery' do
        expect(Authorization::UserEntityRolesQuery).to receive(:query).with(user, context)
        described_class.roles(user, context)
      end
    end

    context 'without a context' do
      let(:context) { nil }

      it 'calls Authorization::UserGlobalRolesQuery' do
        expect(Authorization::UserGlobalRolesQuery).to receive(:query).with(user)
        described_class.roles(user, context)
      end
    end
  end

  describe '.permissions_for' do
    let(:raise_on_unknown) { false }

    subject { described_class.permissions_for(action, raise_on_unknown:) }

    context 'when called with a Permission object' do
      let(:action) { OpenProject::AccessControl.permission(:view_work_packages) }

      it 'returns the Permission object wrapped in an array' do
        expect(subject).to eq([action])
      end
    end

    context 'when called with an array of Permission objects' do
      let(:action) do
        [
          OpenProject::AccessControl.permission(:view_work_packages),
          OpenProject::AccessControl.permission(:edit_work_packages)
        ]
      end

      it 'returns the Permission array' do
        expect(subject).to eq(action)
      end
    end

    context 'when action is a Hash where controller starts with a slash' do
      let(:action) do
        { controller: '/work_packages', action: 'show' }
      end

      it 'returns all permissions that grant the permission to this URL' do
        # there might be more permissions granting access to this URL, we check for a known one
        expect(subject).to include(OpenProject::AccessControl.permission(:view_work_packages))
      end
    end

    context 'when action is a Hash where controller does not start with a slash' do
      let(:action) do
        { controller: 'work_packages', action: 'show' }
      end

      it 'returns all permissions that grant the permission to this URL' do
        # there might be more permissions granting access to this URL, we check for a known one
        expect(subject).to include(OpenProject::AccessControl.permission(:view_work_packages))
      end
    end

    context 'when action is a permission name' do
      let(:action) { :view_work_packages }

      it 'returns the Permission object wrapped in an array' do
        expect(subject).to eq([OpenProject::AccessControl.permission(:view_work_packages)])
      end
    end

    context 'when there is a permission but it is disabled' do
      let(:permission_object) { OpenProject::AccessControl.permission(:manage_user) }
      let(:action) { permission_object.name }

      around do |example|
        permission_object.disable!
        OpenProject::AccessControl.clear_caches
        example.run
      ensure
        permission_object.enable!
        OpenProject::AccessControl.clear_caches
      end

      it 'returns an empty array and does not warn or raise' do
        expect(Rails.logger).not_to receive(:debug)
        expect do
          expect(subject).to be_empty
        end.not_to raise_error
      end
    end

    context 'when there is no permission' do
      let(:action) { :this_permission_does_not_exist }

      context 'and raise_on_unknown is false' do
        let(:raise_on_unknown) { false }

        it 'warns and returns an empty array' do
          allow(Rails.logger).to receive(:debug)

          expect(subject).to be_empty

          expect(Rails.logger).to have_received(:debug) do |_, &block|
            expect(block.call).to include("Used permission \"#{action}\" that is not defined.")
          end
        end
      end

      context 'and raise_on_unknown is true' do
        let(:raise_on_unknown) { true }

        it 'warns and raises' do
          allow(Rails.logger).to receive(:debug)

          expect { subject }.to raise_error(Authorization::UnknownPermissionError)

          expect(Rails.logger).to have_received(:debug) do |_, &block|
            expect(block.call).to include("Used permission \"#{action}\" that is not defined.")
          end
        end
      end
    end

    describe '.contextual_permissions' do
      subject { described_class.contextual_permissions(action, context, raise_on_unknown:) }

      let(:raise_on_unknown) { false }
      let(:context) { nil }

      let(:global_permission) { OpenProject::AccessControl.permission(:manage_user) }
      let(:project_permission) { OpenProject::AccessControl.permission(:manage_members) }
      let(:project_and_work_package_permission) { OpenProject::AccessControl.permission(:view_work_packages) }

      let(:returned_permissions) do
        [
          global_permission,
          project_permission,
          project_and_work_package_permission
        ]
      end

      before do
        allow(described_class).to receive(:permissions_for).and_return(returned_permissions)
      end

      context 'with global context' do
        let(:context) { :global }

        context 'when a global permission is part of the returned permissions' do
          it 'returns only the global permission' do
            expect(subject).to eq([global_permission])
          end
        end

        context 'when no global permission is part of the returned permissions' do
          let(:returned_permissions) { [project_permission, project_and_work_package_permission] }

          it 'raises an IllegalPermissionContextError' do
            expect { subject }.to raise_error(Authorization::IllegalPermissionContextError)
          end
        end
      end

      context 'with project context' do
        let(:context) { :project }

        context 'when a project permission is part of the returned permissions' do
          it 'returns only the project permissions' do
            expect(subject).to eq([project_permission, project_and_work_package_permission])
          end
        end

        context 'when no project permission is part of the returned permissions' do
          let(:returned_permissions) { [global_permission] }

          it 'raises an IllegalPermissionContextError' do
            expect { subject }.to raise_error(Authorization::IllegalPermissionContextError)
          end
        end
      end

      context 'with work package context' do
        let(:context) { :work_package }

        context 'when a work package permission is part of the returned permissions' do
          it 'returns only the work package permission' do
            expect(subject).to eq([project_and_work_package_permission])
          end
        end

        context 'when no work package permission is part of the returned permissions' do
          let(:returned_permissions) { [global_permission, project_permission] }

          it 'raises an IllegalPermissionContextError' do
            expect { subject }.to raise_error(Authorization::IllegalPermissionContextError)
          end
        end
      end
    end
  end
end