Merge branch 'develop' into feat/read-only-token
This commit is contained in:
@@ -149,6 +149,35 @@ describe Messages::MessageBuilder do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is_voice_message is true' do
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
content: 'test',
|
||||
attachments: [Rack::Test::UploadedFile.new('spec/assets/sample.ogg', 'audio/ogg')],
|
||||
is_voice_message: true
|
||||
})
|
||||
end
|
||||
|
||||
it 'sets is_voice_message in attachment meta' do
|
||||
message = message_builder
|
||||
expect(message.attachments.first.meta).to include('is_voice_message' => true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when is_voice_message is not provided' do
|
||||
let(:params) do
|
||||
ActionController::Parameters.new({
|
||||
content: 'test',
|
||||
attachments: [Rack::Test::UploadedFile.new('spec/assets/avatar.png', 'image/png')]
|
||||
})
|
||||
end
|
||||
|
||||
it 'does not set is_voice_message in attachment meta' do
|
||||
message = message_builder
|
||||
expect(message.attachments.first.meta).not_to include('is_voice_message')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when email channel messages' do
|
||||
let!(:channel_email) { create(:channel_email, account: account) }
|
||||
let(:inbox_member) { create(:inbox_member, inbox: channel_email.inbox) }
|
||||
|
||||
@@ -126,32 +126,47 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
Conversations::UnreadCounts::Store.clear_account!(account.id)
|
||||
end
|
||||
|
||||
it 'returns unread conversation counts scoped to the signed-in user' do
|
||||
create_unread_conversation(account: account, inbox: visible_inbox, labels: [label.title])
|
||||
create_unread_conversation(account: account, inbox: hidden_inbox, labels: [label.title])
|
||||
context 'when conversation unread counts feature is enabled' do
|
||||
before do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
end
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
it 'returns unread conversation counts scoped to the signed-in user' do
|
||||
create_unread_conversation(account: account, inbox: visible_inbox, labels: [label.title])
|
||||
create_unread_conversation(account: account, inbox: hidden_inbox, labels: [label.title])
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']).to eq(
|
||||
'inboxes' => { visible_inbox.id.to_s => 1 },
|
||||
'labels' => { label.id.to_s => 1 },
|
||||
'teams' => {}
|
||||
)
|
||||
get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']).to eq(
|
||||
'inboxes' => { visible_inbox.id.to_s => 1 },
|
||||
'labels' => { label.id.to_s => 1 },
|
||||
'teams' => {}
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns unread team conversation counts scoped to the signed-in user' do
|
||||
create_unread_conversation(account: account, inbox: visible_inbox, team: team)
|
||||
create_unread_conversation(account: account, inbox: hidden_inbox, team: team)
|
||||
|
||||
get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']['teams']).to eq(team.id.to_s => 1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'returns unread team conversation counts scoped to the signed-in user' do
|
||||
create_unread_conversation(account: account, inbox: visible_inbox, team: team)
|
||||
create_unread_conversation(account: account, inbox: hidden_inbox, team: team)
|
||||
|
||||
it 'returns forbidden when conversation unread counts feature is disabled' do
|
||||
get "/api/v1/accounts/#{account.id}/conversations/unread_counts",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.parsed_body['payload']['teams']).to eq(team.id.to_s => 1)
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
expect(response.parsed_body['error']).to eq('Conversation unread counts feature not enabled for this account')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -833,6 +848,7 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
end
|
||||
|
||||
it 'refreshes unread count cache when conversation is marked read' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
conversation.update!(agent_last_seen_at: 1.hour.ago)
|
||||
create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming, created_at: 5.minutes.ago)
|
||||
Conversations::UnreadCounts::Builder.new(account).build_base!
|
||||
@@ -920,6 +936,7 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
end
|
||||
|
||||
it 'refreshes unread count cache when conversation is marked unread' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
conversation.update!(agent_last_seen_at: 1.minute.from_now, assignee_last_seen_at: 1.minute.from_now)
|
||||
Conversations::UnreadCounts::Builder.new(account).build_base!
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ RSpec.describe 'Microsoft Authorization API', type: :request do
|
||||
]
|
||||
expect(params['scope']).to eq(expected_scope)
|
||||
expect(params['redirect_uri']).to eq(["#{ENV.fetch('FRONTEND_URL', 'http://localhost:3000')}/microsoft/callback"])
|
||||
expect(url).not_to match(/(?:\?|&)prompt=/)
|
||||
|
||||
# Validate state parameter exists and can be decoded back to the account
|
||||
expect(params['state']).to be_present
|
||||
|
||||
@@ -140,7 +140,7 @@ describe PortalHelper do
|
||||
context 'when theme is not present' do
|
||||
it 'returns the correct link' do
|
||||
expect(helper.generate_home_link('portal_slug', 'en', nil, true)).to eq(
|
||||
'/hc/portal_slug/en'
|
||||
'/hc/portal_slug/en?show_plain_layout=true'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -148,7 +148,7 @@ describe PortalHelper do
|
||||
context 'when theme is present and plain layout is enabled' do
|
||||
it 'returns the correct link' do
|
||||
expect(helper.generate_home_link('portal_slug', 'en', 'dark', true)).to eq(
|
||||
'/hc/portal_slug/en?theme=dark'
|
||||
'/hc/portal_slug/en?show_plain_layout=true&theme=dark'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -172,7 +172,7 @@ describe PortalHelper do
|
||||
theme: nil,
|
||||
is_plain_layout_enabled: true
|
||||
)).to eq(
|
||||
'/hc/portal_slug/en/categories/category_slug'
|
||||
'/hc/portal_slug/en/categories/category_slug?show_plain_layout=true'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -186,7 +186,7 @@ describe PortalHelper do
|
||||
theme: 'dark',
|
||||
is_plain_layout_enabled: true
|
||||
)).to eq(
|
||||
'/hc/portal_slug/en/categories/category_slug?theme=dark'
|
||||
'/hc/portal_slug/en/categories/category_slug?show_plain_layout=true&theme=dark'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -210,7 +210,7 @@ describe PortalHelper do
|
||||
context 'when theme is not present' do
|
||||
it 'returns the correct link' do
|
||||
expect(helper.generate_article_link('portal_slug', 'article_slug', nil, true)).to eq(
|
||||
'/hc/portal_slug/articles/article_slug'
|
||||
'/hc/portal_slug/articles/article_slug?show_plain_layout=true'
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -218,7 +218,7 @@ describe PortalHelper do
|
||||
context 'when theme is present and plain layout is enabled' do
|
||||
it 'returns the correct link' do
|
||||
expect(helper.generate_article_link('portal_slug', 'article_slug', 'dark', true)).to eq(
|
||||
'/hc/portal_slug/articles/article_slug?theme=dark'
|
||||
'/hc/portal_slug/articles/article_slug?show_plain_layout=true&theme=dark'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -237,6 +237,10 @@ describe ActionCableListener do
|
||||
let!(:agent_without_inbox_access) { create(:user, account: account, role: :agent) }
|
||||
let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation) }
|
||||
|
||||
before do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
end
|
||||
|
||||
it 'sends a lightweight refresh event to inbox agents and admins' do
|
||||
expect(conversation.inbox.reload.inbox_members.count).to eq(1)
|
||||
|
||||
@@ -261,6 +265,14 @@ describe ActionCableListener do
|
||||
listener.conversation_unread_count_changed(event)
|
||||
end
|
||||
|
||||
it 'does not broadcast when conversation unread counts feature is disabled' do
|
||||
account.disable_features!(:conversation_unread_counts)
|
||||
|
||||
expect(ActionCableBroadcastJob).not_to receive(:perform_later)
|
||||
|
||||
listener.conversation_unread_count_changed(event)
|
||||
end
|
||||
|
||||
it 'supports deleted conversation data' do
|
||||
event = Events::Base.new(
|
||||
event_name,
|
||||
|
||||
@@ -50,6 +50,44 @@ RSpec.describe Account do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'conversation unread counts feature flag' do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:store) { Conversations::UnreadCounts::Store }
|
||||
let(:inbox_key) { store.inbox_key(account.id, inbox.id) }
|
||||
|
||||
after do
|
||||
store.clear_account!(account.id)
|
||||
end
|
||||
|
||||
it 'clears unread count cache when the feature is enabled' do
|
||||
build_unread_count_cache
|
||||
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
|
||||
expect(store.base_ready?(account.id)).to be(false)
|
||||
expect(store.assignment_ready?(account.id)).to be(false)
|
||||
expect(store.counts_for_keys([inbox_key])).to eq(inbox_key => 0)
|
||||
end
|
||||
|
||||
it 'clears unread count cache when the feature is disabled' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
build_unread_count_cache
|
||||
|
||||
account.disable_features!(:conversation_unread_counts)
|
||||
|
||||
expect(store.base_ready?(account.id)).to be(false)
|
||||
expect(store.assignment_ready?(account.id)).to be(false)
|
||||
expect(store.counts_for_keys([inbox_key])).to eq(inbox_key => 0)
|
||||
end
|
||||
|
||||
def build_unread_count_cache
|
||||
store.mark_base_ready!(account.id)
|
||||
store.mark_assignment_ready!(account.id)
|
||||
store.add_base_membership(account_id: account.id, inbox_id: inbox.id, label_ids: [], conversation_id: 1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'inbound_email_domain' do
|
||||
let(:account) { create(:account) }
|
||||
|
||||
|
||||
@@ -20,6 +20,15 @@ RSpec.describe Article do
|
||||
expect(article).not_to be_valid
|
||||
expect(article.errors[:content]).to include("can't be blank")
|
||||
end
|
||||
|
||||
it 'rejects reserved slugs that collide with help center routes' do
|
||||
Article::RESERVED_SLUGS.each do |reserved_slug|
|
||||
article = build(:article, portal_id: portal_1.id, author_id: user.id, category_id: category_1.id,
|
||||
title: reserved_slug, slug: reserved_slug, content: 'content')
|
||||
expect(article).not_to be_valid
|
||||
expect(article.errors[:slug]).to include('is reserved')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'associations' do
|
||||
|
||||
@@ -11,6 +11,7 @@ RSpec.describe Conversations::UnreadCounts::Listener do
|
||||
end
|
||||
|
||||
it 'refreshes unread counts when an incoming message is created' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
message = create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming)
|
||||
event = Events::Base.new('message.created', Time.zone.now, message: message)
|
||||
|
||||
@@ -29,6 +30,17 @@ RSpec.describe Conversations::UnreadCounts::Listener do
|
||||
expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'ignores incoming message creation when conversation unread counts are disabled' do
|
||||
message = create(:message, account: account, inbox: conversation.inbox, conversation: conversation, message_type: :incoming)
|
||||
event = Events::Base.new('message.created', Time.zone.now, message: message)
|
||||
|
||||
expect(message).not_to receive(:conversation)
|
||||
|
||||
listener.message_created(event)
|
||||
|
||||
expect(Conversations::UnreadCounts::Notifier).not_to have_received(:new)
|
||||
end
|
||||
|
||||
it 'refreshes unread counts when conversation status changes' do
|
||||
changed_attributes = { 'status' => %w[open resolved] }
|
||||
event = Events::Base.new('conversation.status_changed', Time.zone.now, conversation: conversation, changed_attributes: changed_attributes)
|
||||
@@ -78,6 +90,7 @@ RSpec.describe Conversations::UnreadCounts::Listener do
|
||||
end
|
||||
|
||||
it 'removes unread count memberships when a conversation is deleted' do
|
||||
account.enable_features!(:conversation_unread_counts)
|
||||
label = create(:label, account: account)
|
||||
team = create(:team, account: account)
|
||||
assignee = create(:user, account: account)
|
||||
|
||||
@@ -6,6 +6,7 @@ RSpec.describe Conversations::UnreadCounts::Notifier do
|
||||
let(:refresh_result) { true }
|
||||
|
||||
before do
|
||||
conversation.account.enable_features!(:conversation_unread_counts)
|
||||
allow(Conversations::UnreadCounts::Refresher).to receive(:new).and_return(refresher)
|
||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||
end
|
||||
@@ -29,4 +30,19 @@ RSpec.describe Conversations::UnreadCounts::Notifier do
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when conversation unread counts feature is disabled' do
|
||||
before do
|
||||
conversation.account.disable_features!(:conversation_unread_counts)
|
||||
allow(Conversations::UnreadCounts::Store).to receive(:clear_account!)
|
||||
end
|
||||
|
||||
it 'does not refresh, clear cache, or dispatch unread count changed event' do
|
||||
described_class.new(conversation).perform
|
||||
|
||||
expect(Conversations::UnreadCounts::Refresher).not_to have_received(:new)
|
||||
expect(Conversations::UnreadCounts::Store).not_to have_received(:clear_account!)
|
||||
expect(Rails.configuration.dispatcher).not_to have_received(:dispatch)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Messages::WebhookContentNormalizer do
|
||||
describe '.normalize' do
|
||||
it 'returns nil unchanged' do
|
||||
expect(described_class.normalize(nil)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns blank string unchanged' do
|
||||
expect(described_class.normalize('')).to eq('')
|
||||
end
|
||||
|
||||
it 'strips trailing newlines added by TipTap/ProseMirror' do
|
||||
expect(described_class.normalize("hello\n\n\n")).to eq('hello')
|
||||
end
|
||||
|
||||
it 'preserves intentional trailing spaces' do
|
||||
expect(described_class.normalize("hello \n\n")).to eq('hello ')
|
||||
end
|
||||
|
||||
it 'replaces CommonMark hard line breaks (backslash-newline) with plain newlines' do
|
||||
expect(described_class.normalize("hello\\\nworld")).to eq("hello\nworld")
|
||||
end
|
||||
|
||||
it 'replaces CommonMark hard line breaks with CRLF with plain newlines' do
|
||||
expect(described_class.normalize("hello\\\r\nworld")).to eq("hello\nworld")
|
||||
end
|
||||
|
||||
it 'preserves intentional internal newlines' do
|
||||
expect(described_class.normalize("line one\nline two")).to eq("line one\nline two")
|
||||
end
|
||||
|
||||
it 'strips trailing CRLF newlines without leaving dangling carriage returns' do
|
||||
expect(described_class.normalize("hello\r\n\r\n")).to eq('hello')
|
||||
end
|
||||
|
||||
it 'handles both hard line breaks and trailing newlines together' do
|
||||
expect(described_class.normalize("hello\\\nworld\n\n\n")).to eq("hello\nworld")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -60,7 +60,7 @@ describe Whatsapp::Providers::WhatsappCloudService 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')
|
||||
stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
@@ -79,7 +79,7 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
|
||||
# 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')
|
||||
stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
@@ -91,6 +91,41 @@ describe Whatsapp::Providers::WhatsappCloudService do
|
||||
.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 audio voice message with voice flag' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :audio, meta: { 'is_voice_message' => true })
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'voice.ogg', content_type: 'audio/ogg')
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
type: 'audio',
|
||||
audio: WebMock::API.hash_including({ link: anything, voice: true })
|
||||
})
|
||||
)
|
||||
.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 regular audio attachment without voice flag' do
|
||||
attachment = message.attachments.new(account_id: message.account_id, file_type: :audio)
|
||||
attachment.file.attach(io: Rails.root.join('spec/assets/sample.ogg').open, filename: 'audio.ogg', content_type: 'audio/ogg')
|
||||
|
||||
stub_request(:post, 'https://graph.facebook.com/v24.0/123456789/messages')
|
||||
.with(
|
||||
body: hash_including({
|
||||
messaging_product: 'whatsapp',
|
||||
to: '+123456789',
|
||||
type: 'audio'
|
||||
})
|
||||
)
|
||||
.to_return(status: 200, body: whatsapp_response.to_json, headers: response_headers)
|
||||
|
||||
result = service.send_message('+123456789', message)
|
||||
expect(result).to eq 'message_id'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user