Files
74db16158d feat: migrate dyte integration to cloudflare realtimekit (#14752)
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>
2026-06-24 11:04:08 +04:00

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