Dyte is sunsetting its existing infrastructure after the Cloudflare acquisition, so this migrates Chatwoot’s video call integration to Cloudflare RealtimeKit. The integration now uses Cloudflare Account ID, RealtimeKit App ID, and a Cloudflare API token with Realtime Admin permissions. Meeting creation and participant token generation now call Cloudflare’s RealtimeKit APIs, while the existing Chatwoot call experience remains unchanged for agents and customers. This also adds setup-time credential validation, so admins get clearer errors when the API token is invalid, the Cloudflare account or permissions are incorrect, or the RealtimeKit App ID does not belong to the selected account. Fixes https://linear.app/chatwoot/issue/PLA-176/migrate-dyte-integration-to-cloudflare-realtimekit **How to test** 1. Go to Settings → Integrations → Cloudflare RealtimeKit. 2. Add a Cloudflare Account ID, RealtimeKit App ID, and API token with Realtime Admin permissions. 3. Confirm the integration saves successfully with valid credentials. 4. Try invalid credentials and confirm the error identifies whether the token, account/permissions, or app ID is wrong. 5. Start a video call from a conversation and confirm the RealtimeKit meeting opens. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Sony Mathew <sony@chatwoot.com>
172 lines
6.3 KiB
Ruby
172 lines
6.3 KiB
Ruby
require 'rails_helper'
|
|
|
|
describe Dyte do
|
|
let(:dyte_client) { described_class.new('account_id', 'app_id', 'api_token') }
|
|
let(:headers) { { 'Content-Type' => 'application/json' } }
|
|
|
|
it 'raises an exception if account ID, app ID, or API token is absent' do
|
|
expect { described_class.new }.to raise_error(StandardError)
|
|
end
|
|
|
|
context 'when create_a_meeting is called' do
|
|
context 'when API response is success' do
|
|
before do
|
|
stub_request(:post, 'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings')
|
|
.to_return(
|
|
status: 200,
|
|
body: { success: true, data: { id: 'meeting_id' } }.to_json,
|
|
headers: headers
|
|
)
|
|
end
|
|
|
|
it 'returns api response' do
|
|
response = dyte_client.create_a_meeting('title_of_the_meeting')
|
|
expect(response).to eq({ 'id' => 'meeting_id' })
|
|
end
|
|
end
|
|
|
|
context 'when API response is invalid' do
|
|
before do
|
|
stub_request(:post, 'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings')
|
|
.to_return(status: 422, body: { message: 'Title is required' }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns error code with data' do
|
|
response = dyte_client.create_a_meeting('')
|
|
expect(response).to eq({ error: { 'message' => 'Title is required' }, error_code: 422 })
|
|
end
|
|
end
|
|
|
|
context 'when API response succeeds without data' do
|
|
before do
|
|
stub_request(:post, 'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings')
|
|
.to_return(status: 200, body: { success: true, data: nil }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns an explicit unexpected response error' do
|
|
response = dyte_client.create_a_meeting('title_of_the_meeting')
|
|
expect(response).to eq({ error: :unexpected_response, error_code: 200 })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when add_participant_to_meeting is called' do
|
|
let(:participants_url) { 'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings/m_id/participants' }
|
|
|
|
context 'when API parameters are missing' do
|
|
it 'raises an exception' do
|
|
expect { dyte_client.add_participant_to_meeting }.to raise_error(StandardError)
|
|
end
|
|
end
|
|
|
|
context 'when API response is success' do
|
|
before do
|
|
stub_request(:post, participants_url)
|
|
.to_return(
|
|
status: 200,
|
|
body: { success: true, data: { id: 'random_uuid', token: 'json-web-token' } }.to_json,
|
|
headers: headers
|
|
)
|
|
end
|
|
|
|
it 'returns api response' do
|
|
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
|
|
expect(response).to eq({ 'id' => 'random_uuid', 'token' => 'json-web-token' })
|
|
expect(WebMock).to(
|
|
have_requested(:post, participants_url).with { |request| JSON.parse(request.body)['preset_name'] == 'group-call-host' }
|
|
)
|
|
end
|
|
end
|
|
|
|
context 'when API response is invalid' do
|
|
before do
|
|
stub_request(:post, participants_url)
|
|
.to_return(status: 422, body: { message: 'Meeting ID is invalid' }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns error code with data' do
|
|
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
|
|
expect(response).to eq({ error: { 'message' => 'Meeting ID is invalid' }, error_code: 422 })
|
|
end
|
|
end
|
|
|
|
context 'when the default preset is not found' do
|
|
before do
|
|
stub_request(:post, participants_url)
|
|
.with { |request| JSON.parse(request.body)['preset_name'] == 'group-call-host' }
|
|
.to_return(
|
|
status: 404,
|
|
body: { success: false, error: { code: 404, message: 'ResourceNotFound: No preset found with name group-call-host' } }.to_json,
|
|
headers: headers
|
|
)
|
|
|
|
stub_request(:post, participants_url)
|
|
.with { |request| JSON.parse(request.body)['preset_name'] == 'group_call_host' }
|
|
.to_return(
|
|
status: 200,
|
|
body: { success: true, data: { id: 'random_uuid', token: 'json-web-token' } }.to_json,
|
|
headers: headers
|
|
)
|
|
end
|
|
|
|
it 'retries with the legacy Dyte preset name' do
|
|
response = dyte_client.add_participant_to_meeting('m_id', 'c_id', 'name', 'https://avatar.url')
|
|
|
|
expect(response).to eq({ 'id' => 'random_uuid', 'token' => 'json-web-token' })
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when refresh_participant_token is called' do
|
|
let(:participant_token_url) do
|
|
'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings/m_id/participants/participant_id/token'
|
|
end
|
|
|
|
context 'when API response is success' do
|
|
before do
|
|
stub_request(:post, participant_token_url)
|
|
.to_return(status: 200, body: { success: true, data: { token: 'refreshed-json-web-token' } }.to_json, headers: headers)
|
|
end
|
|
|
|
it 'returns a refreshed participant token' do
|
|
response = dyte_client.refresh_participant_token('m_id', 'participant_id')
|
|
|
|
expect(response).to eq({ 'token' => 'refreshed-json-web-token' })
|
|
end
|
|
end
|
|
|
|
context 'when API parameters are missing' do
|
|
it 'raises an exception' do
|
|
expect { dyte_client.refresh_participant_token('m_id', nil) }.to raise_error(StandardError)
|
|
end
|
|
end
|
|
end
|
|
|
|
context 'when fetch_participants is called' do
|
|
let(:participants_url) { 'https://api.cloudflare.com/client/v4/accounts/account_id/realtime/kit/app_id/meetings/m_id/participants' }
|
|
|
|
context 'when API response is success' do
|
|
before do
|
|
stub_request(:get, participants_url)
|
|
.to_return(
|
|
status: 200,
|
|
body: { success: true, data: [{ id: 'participant_id', custom_participant_id: 'c_id' }] }.to_json,
|
|
headers: headers
|
|
)
|
|
end
|
|
|
|
it 'returns participants' do
|
|
response = dyte_client.fetch_participants('m_id')
|
|
|
|
expect(response).to eq([{ 'id' => 'participant_id', 'custom_participant_id' => 'c_id' }])
|
|
end
|
|
end
|
|
|
|
context 'when API parameters are missing' do
|
|
it 'raises an exception' do
|
|
expect { dyte_client.fetch_participants(nil) }.to raise_error(StandardError)
|
|
end
|
|
end
|
|
end
|
|
end
|