chatwoot/spec/services/tiktok/client_spec.rb
Muhsin Keloth b8108b71c1
fix(tiktok): Resolve media upload failures and gate attachments by conversation capability (#13643)
This PR fixes TikTok attachment send failures and adds a
capability-based guard so attachments are only enabled for conversations
that support media sending.

- Fixed TikTok media upload request formatting so TikTok accepts image
uploads reliably.
- Added TikTok capability check (IMAGE_SEND) during conversation
creation.
- Stored capability in
conversation.additional_attributes.tiktok_capabilities.
- Updated reply composer UI to disable/hide attachment upload for TikTok
conversations where image_send is false.

Fixes
https://linear.app/chatwoot/issue/CW-6532/enable-attachments-based-on-the-conversation-capability
and
https://linear.app/chatwoot/issue/CW-6996/unable-to-send-image-attachments-to-tiktok-customer-400-parsing-error

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-06 11:21:15 +04:00

137 lines
4.4 KiB
Ruby

require 'rails_helper'
RSpec.describe Tiktok::Client do
let(:client) { described_class.new(business_id: 'biz-123', access_token: 'token-123') }
let(:response) { instance_double(HTTParty::Response) }
describe '#image_send_capable?' do
before do
allow(HTTParty).to receive(:get).and_return(response)
allow(GlobalConfigService).to receive(:load).with('TIKTOK_API_VERSION', 'v1.3').and_return('v1.3')
end
it 'returns true when IMAGE_SEND capability is enabled' do
allow(client).to receive(:process_json_response).with(
response,
'Failed to fetch TikTok message capabilities'
).and_return(
{
'data' => {
'capability_infos' => [
{ 'capability_type' => 'IMAGE_SEND', 'capability_result' => true }
]
}
}
)
result = client.image_send_capable?('tt-conv-1')
expect(result).to be(true)
expect(HTTParty).to have_received(:get).with(
'https://business-api.tiktok.com/open_api/v1.3/business/message/capabilities/get/',
query: {
business_id: 'biz-123',
conversation_id: 'tt-conv-1',
conversation_type: 'SINGLE',
capability_types: '["IMAGE_SEND"]'
},
headers: { 'Access-Token': 'token-123' }
)
end
it 'returns false when IMAGE_SEND capability is not enabled' do
allow(client).to receive(:process_json_response).with(
response,
'Failed to fetch TikTok message capabilities'
).and_return(
{
'data' => {
'capability_infos' => [
{ 'capability_type' => 'IMAGE_SEND', 'capability_result' => false }
]
}
}
)
result = client.image_send_capable?('tt-conv-1')
expect(result).to be(false)
end
end
describe '#upload_media' do
let(:connection) { instance_double(Faraday::Connection) }
let(:request) { instance_double(Faraday::Request, headers: {}) }
let(:response) { instance_double(Faraday::Response, success?: true, body: response_body) }
let(:response_body) do
{
code: 0,
message: 'OK',
data: { media_id: 'media-123' }
}.to_json
end
let(:blob) do
instance_double(
ActiveStorage::Blob,
content_type: 'image/png',
filename: ActiveStorage::Filename.new('avatar.png')
)
end
before do
allow(GlobalConfigService).to receive(:load).with('TIKTOK_API_VERSION', 'v1.3').and_return('v1.3')
allow(Faraday).to receive(:new).and_return(connection)
allow(blob).to receive(:open) do |&block|
File.open(Rails.root.join('spec/assets/avatar.png'), 'rb', &block)
end
end
it 'posts media upload with access token header' do
captured_endpoint = nil
allow(connection).to receive(:post) do |endpoint, _payload, &block|
captured_endpoint = endpoint
block.call(request)
response
end
media_id = client.send(:upload_media, blob)
expect(media_id).to eq('media-123')
expect(captured_endpoint).to eq('https://business-api.tiktok.com/open_api/v1.3/business/message/media/upload/')
expect(request.headers['Access-Token']).to eq('token-123')
end
it 'uploads media as a multipart file with filename and content type' do
captured_payload = nil
allow(connection).to receive(:post) do |_endpoint, payload, &block|
captured_payload = payload
block.call(request)
response
end
client.send(:upload_media, blob)
expect(captured_payload[:business_id]).to eq('biz-123')
expect(captured_payload[:media_type]).to eq('IMAGE')
expect(captured_payload[:file]).to be_a(Faraday::Multipart::FilePart)
expect(captured_payload[:file].content_type).to eq('image/png')
expect(captured_payload[:file].original_filename).to eq('avatar.png')
end
end
describe '#send_media_message' do
let(:file) { Struct.new(:blob).new('blob') }
let(:attachment) { instance_double(Attachment, file: file) }
it 'sends image messages' do
allow(client).to receive(:upload_media).with('blob', 'IMAGE').and_return('media-123')
allow(client).to receive(:send_message).and_return('tt-msg-123')
message_id = client.send_media_message('tt-conv-1', attachment)
expect(message_id).to eq('tt-msg-123')
expect(client).to have_received(:send_message).with('tt-conv-1', 'IMAGE', 'media-123')
end
end
end