mirror of
https://github.com/lingble/chatwoot.git
synced 2025-10-29 18:22:53 +00:00
chore: Automate SSL with Cloudflare (#12021)
This PR adds support for automatic SSL issuance using Cloudflare when a custom domain is updated. - Introduced a cloudflare configuration. If present, the system will attempt to issue an SSL certificate via Cloudflare whenever a custom domain is added or changed. - SSL verification is handled using an HTTP challenge. - The job will store the HTTP challenge response provided by Cloudflare and serve it under the /.well-known/cf path automatically. How to test: - Create a Cloudflare zone for your domain and copy the Zone ID. - Generate a Cloudflare API token with the required SSL certificate permissions. - Set the Fallback Origin under SSL -> Custom HostName to the Chatwoot installation. - Add or update a custom domain and verify that the SSL certificate is automatically issued. --------- Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
@@ -2,7 +2,7 @@ class MicrosoftController < ApplicationController
|
||||
after_action :set_version_header
|
||||
|
||||
def identity_association
|
||||
microsoft_indentity
|
||||
microsoft_identity
|
||||
end
|
||||
|
||||
private
|
||||
@@ -11,7 +11,7 @@ class MicrosoftController < ApplicationController
|
||||
response.headers['Content-Length'] = { associatedApplications: [{ applicationId: @identity_json }] }.to_json.length
|
||||
end
|
||||
|
||||
def microsoft_indentity
|
||||
def microsoft_identity
|
||||
@identity_json = GlobalConfigService.load('AZURE_APP_ID', nil)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
# name :string not null
|
||||
# page_title :string
|
||||
# slug :string not null
|
||||
# ssl_settings :jsonb not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# account_id :integer not null
|
||||
@@ -69,3 +70,5 @@ class Portal < ApplicationRecord
|
||||
errors.add(:cofig, "in portal on #{denied_keys.join(',')} is not supported.") if denied_keys.any?
|
||||
end
|
||||
end
|
||||
|
||||
Portal.include_mod_with('Concerns::Portal')
|
||||
|
||||
@@ -416,3 +416,17 @@
|
||||
locked: false
|
||||
description: 'The redirect URI configured in your Google OAuth app'
|
||||
## ------ End of Configs added for Google OAuth ------ ##
|
||||
|
||||
## ------ Configs added for Cloudflare ------ ##
|
||||
- name: CLOUDFLARE_API_KEY
|
||||
display_title: 'Cloudflare API Key'
|
||||
value:
|
||||
locked: false
|
||||
description: 'API key for Cloudflare account authentication'
|
||||
type: secret
|
||||
- name: CLOUDFLARE_ZONE_ID
|
||||
display_title: 'Cloudflare Zone ID'
|
||||
value:
|
||||
locked: false
|
||||
description: 'Zone ID for the Cloudflare domain'
|
||||
## ------ End of Configs added for Cloudflare ------ ##
|
||||
|
||||
@@ -517,6 +517,7 @@ Rails.application.routes.draw do
|
||||
get '.well-known/assetlinks.json' => 'android_app#assetlinks'
|
||||
get '.well-known/apple-app-site-association' => 'apple_app#site_association'
|
||||
get '.well-known/microsoft-identity-association.json' => 'microsoft#identity_association'
|
||||
get '.well-known/cf-custom-hostname-challenge/:id', to: 'custom_domains#verify'
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Internal Monitoring Routes
|
||||
|
||||
5
db/migrate/20250722083820_add_ssl_settings_to_portals.rb
Normal file
5
db/migrate/20250722083820_add_ssl_settings_to_portals.rb
Normal file
@@ -0,0 +1,5 @@
|
||||
class AddSslSettingsToPortals < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_column :portals, :ssl_settings, :jsonb, default: {}, null: false
|
||||
end
|
||||
end
|
||||
@@ -943,6 +943,7 @@ ActiveRecord::Schema[7.1].define(version: 2025_07_22_152516) do
|
||||
t.jsonb "config", default: {"allowed_locales" => ["en"]}
|
||||
t.boolean "archived", default: false
|
||||
t.bigint "channel_web_widget_id"
|
||||
t.jsonb "ssl_settings", default: {}, null: false
|
||||
t.index ["channel_web_widget_id"], name: "index_portals_on_channel_web_widget_id"
|
||||
t.index ["custom_domain"], name: "index_portals_on_custom_domain", unique: true
|
||||
t.index ["slug"], name: "index_portals_on_slug", unique: true
|
||||
|
||||
22
enterprise/app/controllers/custom_domains_controller.rb
Normal file
22
enterprise/app/controllers/custom_domains_controller.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
class CustomDomainsController < ApplicationController
|
||||
def verify
|
||||
challenge_id = permitted_params[:id]
|
||||
|
||||
domain = request.host
|
||||
portal = Portal.find_by(custom_domain: domain)
|
||||
|
||||
return render plain: 'Domain not found', status: :not_found unless portal
|
||||
|
||||
ssl_settings = portal.ssl_settings || {}
|
||||
|
||||
return render plain: 'Challenge ID not found', status: :not_found unless ssl_settings['cf_verification_id'] == challenge_id
|
||||
|
||||
render plain: ssl_settings['cf_verification_body'], status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.permit(:id)
|
||||
end
|
||||
end
|
||||
@@ -34,6 +34,6 @@ module Enterprise::SuperAdmin::AppConfigsController
|
||||
def internal_config_options
|
||||
%w[CHATWOOT_INBOX_TOKEN CHATWOOT_INBOX_HMAC_KEY ANALYTICS_TOKEN CLEARBIT_API_KEY DASHBOARD_SCRIPTS INACTIVE_WHATSAPP_NUMBERS BLOCKED_EMAIL_DOMAINS
|
||||
CAPTAIN_CLOUD_PLAN_LIMITS ACCOUNT_SECURITY_NOTIFICATION_WEBHOOK_URL CHATWOOT_INSTANCE_ADMIN_EMAIL
|
||||
OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF]
|
||||
OG_IMAGE_CDN_URL OG_IMAGE_CLIENT_REF CLOUDFLARE_API_KEY CLOUDFLARE_ZONE_ID]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
class Enterprise::CloudflareVerificationJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(portal_id)
|
||||
portal = Portal.find(portal_id)
|
||||
return unless portal && portal.custom_domain.present?
|
||||
|
||||
result = check_hostname_status(portal)
|
||||
|
||||
create_hostname(portal) if result[:errors].present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_hostname(portal)
|
||||
Cloudflare::CreateCustomHostnameService.new(portal: portal).perform
|
||||
end
|
||||
|
||||
def check_hostname_status(portal)
|
||||
Cloudflare::CheckCustomHostnameService.new(portal: portal).perform
|
||||
end
|
||||
end
|
||||
14
enterprise/app/models/enterprise/concerns/portal.rb
Normal file
14
enterprise/app/models/enterprise/concerns/portal.rb
Normal file
@@ -0,0 +1,14 @@
|
||||
module Enterprise::Concerns::Portal
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_save :enqueue_cloudflare_verification, if: :saved_change_to_custom_domain?
|
||||
end
|
||||
|
||||
def enqueue_cloudflare_verification
|
||||
return if custom_domain.blank?
|
||||
return unless ChatwootApp.chatwoot_cloud?
|
||||
|
||||
Enterprise::CloudflareVerificationJob.perform_later(id)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
class Cloudflare::BaseCloudflareZoneService
|
||||
BASE_URI = 'https://api.cloudflare.com/client/v4'.freeze
|
||||
|
||||
private
|
||||
|
||||
def headers
|
||||
{
|
||||
'Authorization' => "Bearer #{api_token}",
|
||||
'Content-Type' => 'application/json'
|
||||
}
|
||||
end
|
||||
|
||||
def api_token
|
||||
InstallationConfig.find_by(name: 'CLOUDFLARE_API_KEY')&.value
|
||||
end
|
||||
|
||||
def zone_id
|
||||
InstallationConfig.find_by(name: 'CLOUDFLARE_ZONE_ID')&.value
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
class Cloudflare::CheckCustomHostnameService < Cloudflare::BaseCloudflareZoneService
|
||||
pattr_initialize [:portal!]
|
||||
|
||||
def perform
|
||||
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
|
||||
return { errors: ['No custom domain found'] } if @portal.custom_domain.blank?
|
||||
|
||||
response = HTTParty.get(
|
||||
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames?hostname=#{@portal.custom_domain}", headers: headers
|
||||
)
|
||||
|
||||
return { errors: response.parsed_response['errors'] } unless response.success?
|
||||
|
||||
data = response.parsed_response['result']
|
||||
|
||||
if data.present?
|
||||
update_portal_ssl_settings(data.first)
|
||||
return { data: data }
|
||||
end
|
||||
|
||||
{ errors: ['Hostname is missing in Cloudflare'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_portal_ssl_settings(data)
|
||||
verification_record = data['ownership_verification_http']
|
||||
ssl_settings = {
|
||||
'cf_verification_id': verification_record['http_url'].split('/').last,
|
||||
'cf_verification_body': verification_record['http_body']
|
||||
}
|
||||
@portal.update(ssl_settings: ssl_settings)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,40 @@
|
||||
class Cloudflare::CreateCustomHostnameService < Cloudflare::BaseCloudflareZoneService
|
||||
pattr_initialize [:portal!]
|
||||
|
||||
def perform
|
||||
return { errors: ['Cloudflare API token or zone ID not found'] } if api_token.blank? || zone_id.blank?
|
||||
return { errors: ['No hostname found'] } if @portal.custom_domain.blank?
|
||||
|
||||
response = create_hostname
|
||||
|
||||
return { errors: response.parsed_response['errors'] } unless response.success?
|
||||
|
||||
data = response.parsed_response['result']
|
||||
|
||||
if data.present?
|
||||
update_portal_ssl_settings(data)
|
||||
return { data: data }
|
||||
end
|
||||
|
||||
{ errors: ['Could not create hostname'] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_hostname
|
||||
HTTParty.post(
|
||||
"#{BASE_URI}/zones/#{zone_id}/custom_hostnames",
|
||||
headers: headers,
|
||||
body: { hostname: @portal.custom_domain }.to_json
|
||||
)
|
||||
end
|
||||
|
||||
def update_portal_ssl_settings(data)
|
||||
verification_record = data['ownership_verification_http']
|
||||
ssl_settings = {
|
||||
'cf_verification_id': verification_record['http_url'].split('/').last,
|
||||
'cf_verification_body': verification_record['http_body']
|
||||
}
|
||||
@portal.update(ssl_settings: ssl_settings)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,47 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::CloudflareVerificationJob do
|
||||
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when portal is not found' do
|
||||
it 'returns early' do
|
||||
expect(Portal).to receive(:find).with(0).and_return(nil)
|
||||
expect(Cloudflare::CheckCustomHostnameService).not_to receive(:new)
|
||||
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when portal has no custom domain' do
|
||||
it 'returns early' do
|
||||
portal_without_domain = create(:portal, custom_domain: nil)
|
||||
expect(Cloudflare::CheckCustomHostnameService).not_to receive(:new)
|
||||
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now(portal_without_domain.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when portal exists with custom domain' do
|
||||
it 'checks hostname status' do
|
||||
check_service = instance_double(Cloudflare::CheckCustomHostnameService, perform: { data: 'success' })
|
||||
expect(Cloudflare::CheckCustomHostnameService).to receive(:new).with(portal: portal).and_return(check_service)
|
||||
expect(Cloudflare::CreateCustomHostnameService).not_to receive(:new)
|
||||
|
||||
described_class.perform_now(portal.id)
|
||||
end
|
||||
|
||||
it 'creates hostname when check returns errors' do
|
||||
check_service = instance_double(Cloudflare::CheckCustomHostnameService, perform: { errors: ['Hostname is missing'] })
|
||||
create_service = instance_double(Cloudflare::CreateCustomHostnameService, perform: { data: 'success' })
|
||||
|
||||
expect(Cloudflare::CheckCustomHostnameService).to receive(:new).with(portal: portal).and_return(check_service)
|
||||
expect(Cloudflare::CreateCustomHostnameService).to receive(:new).with(portal: portal).and_return(create_service)
|
||||
|
||||
described_class.perform_now(portal.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
59
spec/enterprise/models/enterprise/concerns/portal_spec.rb
Normal file
59
spec/enterprise/models/enterprise/concerns/portal_spec.rb
Normal file
@@ -0,0 +1,59 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Enterprise::Concerns::Portal do
|
||||
describe '#enqueue_cloudflare_verification' do
|
||||
let(:portal) { create(:portal, custom_domain: nil) }
|
||||
|
||||
context 'when custom_domain is changed' do
|
||||
context 'when on chatwoot cloud' do
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
end
|
||||
|
||||
it 'enqueues cloudflare verification job' do
|
||||
expect do
|
||||
portal.update(custom_domain: 'test.example.com')
|
||||
end.to have_enqueued_job(Enterprise::CloudflareVerificationJob).with(portal.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when not on chatwoot cloud' do
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(false)
|
||||
end
|
||||
|
||||
it 'does not enqueue cloudflare verification job' do
|
||||
expect do
|
||||
portal.update(custom_domain: 'test.example.com')
|
||||
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom_domain is not changed' do
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
portal.update(custom_domain: 'test.example.com')
|
||||
end
|
||||
|
||||
it 'does not enqueue cloudflare verification job' do
|
||||
expect do
|
||||
portal.update(name: 'New Name')
|
||||
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when custom_domain is set to blank' do
|
||||
before do
|
||||
allow(ChatwootApp).to receive(:chatwoot_cloud?).and_return(true)
|
||||
portal.update(custom_domain: 'test.example.com')
|
||||
end
|
||||
|
||||
it 'does not enqueue cloudflare verification job' do
|
||||
expect do
|
||||
portal.update(custom_domain: '')
|
||||
end.not_to have_enqueued_job(Enterprise::CloudflareVerificationJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Cloudflare::CheckCustomHostnameService do
|
||||
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
|
||||
let(:installation_config_api_key) { create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-key') }
|
||||
let(:installation_config_zone_id) { create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id') }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when API token or zone ID is not found' do
|
||||
it 'returns error when API token is missing' do
|
||||
installation_config_zone_id
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
|
||||
end
|
||||
|
||||
it 'returns error when zone ID is missing' do
|
||||
installation_config_api_key
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no hostname ID is found' do
|
||||
it 'returns error' do
|
||||
installation_config_api_key
|
||||
installation_config_zone_id
|
||||
portal.update(custom_domain: nil)
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['No custom domain found'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request is made' do
|
||||
before do
|
||||
installation_config_api_key
|
||||
installation_config_zone_id
|
||||
end
|
||||
|
||||
context 'when API request fails' do
|
||||
it 'returns error response' do
|
||||
service = described_class.new(portal: portal)
|
||||
error_response = {
|
||||
'errors' => [{ 'message' => 'API error' }]
|
||||
}
|
||||
|
||||
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
|
||||
.to_return(status: 422, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result[:errors]).to eq(error_response['errors'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request succeeds but no data is returned' do
|
||||
it 'returns hostname missing error' do
|
||||
service = described_class.new(portal: portal)
|
||||
success_response = {
|
||||
'result' => []
|
||||
}
|
||||
|
||||
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
|
||||
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Hostname is missing in Cloudflare'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request succeeds and data is returned' do
|
||||
it 'updates portal SSL settings and returns success' do
|
||||
service = described_class.new(portal: portal)
|
||||
success_response = {
|
||||
'result' => [
|
||||
{
|
||||
'ownership_verification_http' => {
|
||||
'http_url' => 'http://example.com/.well-known/cf-verification/verification-id',
|
||||
'http_body' => 'verification-body'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
stub_request(:get, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames?hostname=test.example.com')
|
||||
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
expect(portal).to receive(:update).with(
|
||||
ssl_settings: {
|
||||
'cf_verification_id': 'verification-id',
|
||||
'cf_verification_body': 'verification-body'
|
||||
}
|
||||
)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(data: success_response['result'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,111 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Cloudflare::CreateCustomHostnameService do
|
||||
let(:portal) { create(:portal, custom_domain: 'test.example.com') }
|
||||
let(:installation_config_api_key) { create(:installation_config, name: 'CLOUDFLARE_API_KEY', value: 'test-api-key') }
|
||||
let(:installation_config_zone_id) { create(:installation_config, name: 'CLOUDFLARE_ZONE_ID', value: 'test-zone-id') }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when API token or zone ID is not found' do
|
||||
it 'returns error when API token is missing' do
|
||||
installation_config_zone_id
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
|
||||
end
|
||||
|
||||
it 'returns error when zone ID is missing' do
|
||||
installation_config_api_key
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Cloudflare API token or zone ID not found'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no hostname is found' do
|
||||
it 'returns error' do
|
||||
installation_config_api_key
|
||||
installation_config_zone_id
|
||||
portal.update(custom_domain: nil)
|
||||
service = described_class.new(portal: portal)
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['No hostname found'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request is made' do
|
||||
before do
|
||||
installation_config_api_key
|
||||
installation_config_zone_id
|
||||
end
|
||||
|
||||
context 'when API request fails' do
|
||||
it 'returns error response' do
|
||||
service = described_class.new(portal: portal)
|
||||
error_response = {
|
||||
'errors' => [{ 'message' => 'API error' }]
|
||||
}
|
||||
|
||||
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
|
||||
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
|
||||
body: { hostname: 'test.example.com' }.to_json)
|
||||
.to_return(status: 422, body: error_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result[:errors]).to eq(error_response['errors'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request succeeds but no data is returned' do
|
||||
it 'returns hostname creation error' do
|
||||
service = described_class.new(portal: portal)
|
||||
success_response = {
|
||||
'result' => nil
|
||||
}
|
||||
|
||||
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
|
||||
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
|
||||
body: { hostname: 'test.example.com' }.to_json)
|
||||
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(errors: ['Could not create hostname'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when API request succeeds and data is returned' do
|
||||
it 'updates portal SSL settings and returns success' do
|
||||
service = described_class.new(portal: portal)
|
||||
success_response = {
|
||||
'result' => {
|
||||
'ownership_verification_http' => {
|
||||
'http_url' => 'http://example.com/.well-known/cf-verification/verification-id',
|
||||
'http_body' => 'verification-body'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stub_request(:post, 'https://api.cloudflare.com/client/v4/zones/test-zone-id/custom_hostnames')
|
||||
.with(headers: { 'Authorization' => 'Bearer test-api-key', 'Content-Type' => 'application/json' },
|
||||
body: { hostname: 'test.example.com' }.to_json)
|
||||
.to_return(status: 200, body: success_response.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
|
||||
expect(portal).to receive(:update).with(ssl_settings: { 'cf_verification_id': 'verification-id',
|
||||
'cf_verification_body': 'verification-body' })
|
||||
|
||||
result = service.perform
|
||||
|
||||
expect(result).to eq(data: success_response['result'])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user