Merge remote-tracking branch 'origin/feat/whatsapp-embedded-signup' into feat/whatsapp-embedded-signup
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
require 'rails_helper'
|
||||
|
||||
# rubocop:disable RSpec/DescribeClass
|
||||
describe 'Markdown Embeds Configuration' do
|
||||
# rubocop:enable RSpec/DescribeClass
|
||||
let(:config) { YAML.load_file(Rails.root.join('config/markdown_embeds.yml')) }
|
||||
|
||||
describe 'YAML structure' do
|
||||
it 'loads valid YAML' do
|
||||
expect(config).to be_a(Hash)
|
||||
expect(config).not_to be_empty
|
||||
end
|
||||
|
||||
it 'has required keys for each embed type' do
|
||||
config.each do |embed_type, embed_config|
|
||||
expect(embed_config).to have_key('regex'), "#{embed_type} missing regex"
|
||||
expect(embed_config).to have_key('template'), "#{embed_type} missing template"
|
||||
expect(embed_config['regex']).to be_a(String), "#{embed_type} regex should be string"
|
||||
expect(embed_config['template']).to be_a(String), "#{embed_type} template should be string"
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains expected embed types' do
|
||||
expected_types = %w[youtube loom vimeo mp4 arcade wistia bunny codepen github_gist]
|
||||
expect(config.keys).to match_array(expected_types)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'regex patterns and named capture groups' do
|
||||
let(:test_cases) do
|
||||
{
|
||||
'youtube' => [
|
||||
{ url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', expected: { 'video_id' => 'dQw4w9WgXcQ' } },
|
||||
{ url: 'https://youtu.be/dQw4w9WgXcQ', expected: { 'video_id' => 'dQw4w9WgXcQ' } },
|
||||
{ url: 'https://youtube.com/watch?v=abc123XYZ', expected: { 'video_id' => 'abc123XYZ' } }
|
||||
],
|
||||
'loom' => [
|
||||
{ url: 'https://www.loom.com/share/abc123def456', expected: { 'video_id' => 'abc123def456' } },
|
||||
{ url: 'https://loom.com/share/xyz789', expected: { 'video_id' => 'xyz789' } }
|
||||
],
|
||||
'vimeo' => [
|
||||
{ url: 'https://vimeo.com/123456789', expected: { 'video_id' => '123456789' } },
|
||||
{ url: 'https://www.vimeo.com/987654321', expected: { 'video_id' => '987654321' } }
|
||||
],
|
||||
'mp4' => [
|
||||
{ url: 'https://example.com/video.mp4', expected: { 'link_url' => 'https://example.com/video.mp4' } },
|
||||
{ url: 'https://www.test.com/path/to/movie.mp4', expected: { 'link_url' => 'https://www.test.com/path/to/movie.mp4' } }
|
||||
],
|
||||
'arcade' => [
|
||||
{ url: 'https://app.arcade.software/share/arcade123', expected: { 'video_id' => 'arcade123' } },
|
||||
{ url: 'https://www.app.arcade.software/share/demo456', expected: { 'video_id' => 'demo456' } }
|
||||
],
|
||||
'wistia' => [
|
||||
{ url: 'https://chatwoot.wistia.com/medias/kjwjeq6f9i', expected: { 'video_id' => 'kjwjeq6f9i' } },
|
||||
{ url: 'https://www.company.wistia.com/medias/abc123def', expected: { 'video_id' => 'abc123def' } }
|
||||
],
|
||||
'bunny' => [
|
||||
{ url: 'https://iframe.mediadelivery.net/play/431789/1f105841-cad9-46fe-a70e-b7623c60797c',
|
||||
expected: { 'library_id' => '431789', 'video_id' => '1f105841-cad9-46fe-a70e-b7623c60797c' } },
|
||||
{ url: 'https://iframe.mediadelivery.net/play/12345/abcdef-ghijkl', expected: { 'library_id' => '12345', 'video_id' => 'abcdef-ghijkl' } }
|
||||
],
|
||||
'codepen' => [
|
||||
{ url: 'https://codepen.io/username/pen/abcdef', expected: { 'user' => 'username', 'pen_id' => 'abcdef' } },
|
||||
{ url: 'https://www.codepen.io/testuser/pen/xyz123', expected: { 'user' => 'testuser', 'pen_id' => 'xyz123' } }
|
||||
],
|
||||
'github_gist' => [
|
||||
{ url: 'https://gist.github.com/username/1234567890abcdef1234567890abcdef',
|
||||
expected: { 'username' => 'username', 'gist_id' => '1234567890abcdef1234567890abcdef' } },
|
||||
{ url: 'https://gist.github.com/testuser/fedcba0987654321fedcba0987654321', expected: { 'username' => 'testuser', 'gist_id' => 'fedcba0987654321fedcba0987654321' } }
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'correctly captures named groups for all embed types' do
|
||||
test_cases.each do |embed_type, cases|
|
||||
regex = Regexp.new(config[embed_type]['regex'])
|
||||
|
||||
cases.each do |test_case|
|
||||
match = regex.match(test_case[:url])
|
||||
expect(match).not_to be_nil, "#{embed_type} regex failed to match URL: #{test_case[:url]}"
|
||||
expect(match.named_captures).to eq(test_case[:expected]),
|
||||
"#{embed_type} captured groups don't match expected for URL: #{test_case[:url]}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'validates that template variables match capture group names' do
|
||||
config.each do |embed_type, embed_config|
|
||||
regex = Regexp.new(embed_config['regex'])
|
||||
template = embed_config['template']
|
||||
|
||||
# Extract template variables like %{video_id}
|
||||
template_vars = template.scan(/%\{(\w+)\}/).flatten.uniq
|
||||
|
||||
# Get named capture groups from regex
|
||||
capture_names = regex.names
|
||||
|
||||
expect(capture_names).to match_array(template_vars),
|
||||
"#{embed_type}: Template variables #{template_vars} don't match capture groups #{capture_names}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -926,4 +926,63 @@ RSpec.describe 'Conversations API', type: :request do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'DELETE /api/v1/accounts/{account.id}/conversations/:id' do
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
|
||||
context 'when it is an unauthenticated user' do
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}"
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated agent' do
|
||||
before do
|
||||
create(:inbox_member, user: agent, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
it 'returns unauthorized' do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
response_body = response.parsed_body
|
||||
expect(response_body['error']).to eq('You are not authorized to do this action')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is an authenticated administrator' do
|
||||
before do
|
||||
create(:inbox_member, user: administrator, inbox: conversation.inbox)
|
||||
end
|
||||
|
||||
it 'successfully deletes the conversation' do
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}",
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(DeleteObjectJob).with(conversation, administrator, anything)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
||||
it 'can delete conversations from inboxes without direct access' do
|
||||
other_inbox = create(:inbox, account: account)
|
||||
other_conversation = create(:conversation, account: account, inbox: other_inbox)
|
||||
|
||||
expect do
|
||||
delete "/api/v1/accounts/#{account.id}/conversations/#{other_conversation.display_id}",
|
||||
headers: administrator.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(DeleteObjectJob).with(other_conversation, administrator, anything)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -5,7 +5,7 @@ RSpec.describe 'Integration Hooks API', type: :request do
|
||||
let(:admin) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' } } } }
|
||||
let(:params) { { app_id: 'dialogflow', inbox_id: inbox.id, settings: { project_id: 'xx', credentials: { test: 'test' }, region: 'europe-west1' } } }
|
||||
|
||||
describe 'POST /api/v1/accounts/{account.id}/integrations/hooks' do
|
||||
context 'when it is an unauthenticated user' do
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Messages::AudioTranscriptionJob do
|
||||
subject(:job) { described_class.perform_later(attachment_id) }
|
||||
|
||||
let(:message) { create(:message) }
|
||||
let(:attachment) do
|
||||
message.attachments.create!(
|
||||
account_id: message.account_id,
|
||||
file_type: :audio,
|
||||
file: fixture_file_upload('public/audio/widget/ding.mp3')
|
||||
)
|
||||
end
|
||||
let(:attachment_id) { attachment.id }
|
||||
let(:conversation) { message.conversation }
|
||||
let(:transcription_service) { instance_double(Messages::AudioTranscriptionService) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
expect { job }.to have_enqueued_job(described_class)
|
||||
.with(attachment_id)
|
||||
.on_queue('low')
|
||||
end
|
||||
|
||||
context 'when performing the job' do
|
||||
before do
|
||||
allow(Messages::AudioTranscriptionService).to receive(:new).with(attachment).and_return(transcription_service)
|
||||
allow(transcription_service).to receive(:perform)
|
||||
end
|
||||
|
||||
it 'calls AudioTranscriptionService with the attachment' do
|
||||
expect(Messages::AudioTranscriptionService).to receive(:new).with(attachment)
|
||||
expect(transcription_service).to receive(:perform)
|
||||
described_class.perform_now(attachment_id)
|
||||
end
|
||||
|
||||
it 'does nothing when attachment is not found' do
|
||||
expect(Messages::AudioTranscriptionService).not_to receive(:new)
|
||||
described_class.perform_now(999_999)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Enterprise::CsatSurveyResponsePolicy', type: :policy do
|
||||
subject(:csat_policy) { CsatSurveyResponsePolicy }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:csat_survey_response) { create(:csat_survey_response, account: account) }
|
||||
|
||||
# Create a custom role with report_manage permission
|
||||
let(:custom_role) { create(:custom_role, account: account, permissions: ['report_manage']) }
|
||||
let(:agent_with_role) { create(:user) } # Create without account
|
||||
let(:agent_with_role_account_user) do
|
||||
create(:account_user, user: agent_with_role, account: account, role: :agent, custom_role: custom_role)
|
||||
end
|
||||
let(:agent_with_role_context) do
|
||||
{ user: agent_with_role, account: account, account_user: agent_with_role_account_user }
|
||||
end
|
||||
|
||||
permissions :index?, :metrics?, :download? do
|
||||
context 'when agent with report_manage permission' do
|
||||
it { expect(csat_policy).to permit(agent_with_role_context, csat_survey_response) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -109,4 +109,46 @@ RSpec.describe Captain::ToolRegistryService do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tools_summary' do
|
||||
let(:tool_class) { TestTool }
|
||||
|
||||
before do
|
||||
service.register_tool(tool_class)
|
||||
end
|
||||
|
||||
it 'returns formatted summary of registered tools' do
|
||||
expect(service.tools_summary).to eq('- test_tool: A test tool for specs')
|
||||
end
|
||||
|
||||
context 'when multiple tools are registered' do
|
||||
let(:another_tool_class) do
|
||||
Class.new(Captain::Tools::BaseService) do
|
||||
def name
|
||||
'another_tool'
|
||||
end
|
||||
|
||||
def description
|
||||
'Another test tool'
|
||||
end
|
||||
|
||||
def parameters
|
||||
{
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
end
|
||||
|
||||
def active?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes all tools in the summary' do
|
||||
service.register_tool(another_tool_class)
|
||||
expect(service.tools_summary).to eq("- test_tool: A test tool for specs\n- another_tool: Another test tool")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Messages::AudioTranscriptionService, type: :service do
|
||||
let(:account) { create(:account, audio_transcriptions: true) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:message) { create(:message, conversation: conversation) }
|
||||
let(:attachment) { message.attachments.create!(account: account, file_type: :audio) }
|
||||
|
||||
before do
|
||||
# Create required installation configs
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_API_KEY', value: 'test-api-key')
|
||||
create(:installation_config, name: 'CAPTAIN_OPEN_AI_MODEL', value: 'gpt-4o-mini')
|
||||
|
||||
# Mock usage limits for transcription to be available
|
||||
allow(account).to receive(:usage_limits).and_return({ captain: { responses: { current_available: 100 } } })
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
let(:service) { described_class.new(attachment) }
|
||||
|
||||
context 'when transcription is successful' do
|
||||
before do
|
||||
# Mock can_transcribe? to return true and transcribe_audio method
|
||||
allow(service).to receive(:can_transcribe?).and_return(true)
|
||||
allow(service).to receive(:transcribe_audio).and_return('Hello world transcription')
|
||||
end
|
||||
|
||||
it 'returns successful transcription' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ success: true, transcriptions: 'Hello world transcription' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when audio transcriptions are disabled' do
|
||||
before do
|
||||
account.update!(audio_transcriptions: false)
|
||||
end
|
||||
|
||||
it 'returns error for transcription limit exceeded' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ error: 'Transcription limit exceeded' })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when attachment already has transcribed text' do
|
||||
before do
|
||||
attachment.update!(meta: { transcribed_text: 'Existing transcription' })
|
||||
allow(service).to receive(:can_transcribe?).and_return(true)
|
||||
end
|
||||
|
||||
it 'returns existing transcription without calling API' do
|
||||
result = service.perform
|
||||
expect(result).to eq({ success: true, transcriptions: 'Existing transcription' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -9,7 +9,7 @@ FactoryBot.define do
|
||||
|
||||
trait :dialogflow do
|
||||
app_id { 'dialogflow' }
|
||||
settings { { project_id: 'test', credentials: {} } }
|
||||
settings { { project_id: 'test', credentials: {}, region: 'global' } }
|
||||
end
|
||||
|
||||
trait :dyte do
|
||||
|
||||
@@ -4,6 +4,7 @@ RSpec.describe Conversations::ResolutionJob do
|
||||
subject(:job) { described_class.perform_later(account: account) }
|
||||
|
||||
let!(:account) { create(:account) }
|
||||
let(:label) { create(:label, title: 'auto-resolved', account: account) }
|
||||
let!(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
it 'enqueues the job' do
|
||||
@@ -47,6 +48,16 @@ RSpec.describe Conversations::ResolutionJob do
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds a label after resolution' do
|
||||
account.update(auto_resolve_label: 'auto-resolved', auto_resolve_after: 14_400)
|
||||
conversation = create(:conversation, account: account, last_activity_at: 13.days.ago, waiting_since: 13.days.ago)
|
||||
|
||||
described_class.perform_now(account: account)
|
||||
|
||||
expect(conversation.reload.status).to eq('resolved')
|
||||
expect(conversation.reload.label_list).to include('auto-resolved')
|
||||
end
|
||||
|
||||
it 'resolves only a limited number of conversations in a single execution' do
|
||||
stub_const('Limits::BULK_ACTIONS_LIMIT', 2)
|
||||
account.update(auto_resolve_after: 14_400, auto_resolve_ignore_waiting: false) # 10 days in minutes
|
||||
|
||||
@@ -175,4 +175,65 @@ describe Integrations::Dialogflow::ProcessorService do
|
||||
.to change(hook, :status).from('enabled').to('disabled')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'region configuration' do
|
||||
let(:processor) { described_class.new(event_name: event_name, hook: hook, event_data: event_data) }
|
||||
|
||||
context 'when region is global or not specified' do
|
||||
it 'uses global endpoint and session path' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {} })
|
||||
|
||||
expect(processor.send(:dialogflow_endpoint)).to eq('dialogflow.googleapis.com')
|
||||
expect(processor.send(:build_session_path, 'test-session')).to eq('projects/test-project/agent/sessions/test-session')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when region is specified' do
|
||||
it 'uses regional endpoint and session path' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
|
||||
expect(processor.send(:dialogflow_endpoint)).to eq('europe-west1-dialogflow.googleapis.com')
|
||||
expect(processor.send(:build_session_path, 'test-session')).to eq('projects/test-project/locations/europe-west1/agent/sessions/test-session')
|
||||
end
|
||||
end
|
||||
|
||||
it 'configures client with correct endpoint' do
|
||||
hook.update(settings: { 'project_id' => 'test', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
config = OpenStruct.new
|
||||
expect(Google::Cloud::Dialogflow::V2::Sessions::Client).to receive(:configure).and_yield(config)
|
||||
|
||||
processor.send(:configure_dialogflow_client_defaults)
|
||||
expect(config.endpoint).to eq('europe-west1-dialogflow.googleapis.com')
|
||||
end
|
||||
|
||||
context 'when calling detect_intent' do
|
||||
let(:mock_client) { instance_double(Google::Cloud::Dialogflow::V2::Sessions::Client) }
|
||||
|
||||
before do
|
||||
allow(Google::Cloud::Dialogflow::V2::Sessions::Client).to receive(:new).and_return(mock_client)
|
||||
end
|
||||
|
||||
it 'uses global session path when region is not specified' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {} })
|
||||
|
||||
expect(mock_client).to receive(:detect_intent).with(
|
||||
session: 'projects/test-project/agent/sessions/test-session',
|
||||
query_input: { text: { text: 'Hello', language_code: 'en-US' } }
|
||||
)
|
||||
|
||||
processor.send(:detect_intent, 'test-session', 'Hello')
|
||||
end
|
||||
|
||||
it 'uses regional session path when region is specified' do
|
||||
hook.update(settings: { 'project_id' => 'test-project', 'credentials' => {}, 'region' => 'europe-west1' })
|
||||
|
||||
expect(mock_client).to receive(:detect_intent).with(
|
||||
session: 'projects/test-project/locations/europe-west1/agent/sessions/test-session',
|
||||
query_input: { text: { text: 'Hello', language_code: 'en-US' } }
|
||||
)
|
||||
|
||||
processor.send(:detect_intent, 'test-session', 'Hello')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -435,20 +435,6 @@ RSpec.describe Conversation do
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_csat_not_sent_activity_message' do
|
||||
subject(:create_csat_not_sent_activity_message) { conversation.create_csat_not_sent_activity_message }
|
||||
|
||||
let(:conversation) { create(:conversation) }
|
||||
|
||||
it 'creates CSAT not sent activity message' do
|
||||
create_csat_not_sent_activity_message
|
||||
expect(Conversations::ActivityMessageJob)
|
||||
.to(have_been_enqueued.at_least(:once).with(conversation, { account_id: conversation.account_id, inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: 'CSAT survey not sent due to outgoing message restrictions' }))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'unread_messages' do
|
||||
subject(:unread_messages) { conversation.unread_messages }
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Conversation Audit', type: :model do
|
||||
let(:account) { create(:account) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
before do
|
||||
# Enable auditing for conversations
|
||||
conversation.class.send(:include, Enterprise::Audit::Conversation) if defined?(Enterprise::Audit::Conversation)
|
||||
end
|
||||
|
||||
describe 'audit logging on destroy' do
|
||||
it 'creates an audit log when conversation is destroyed' do
|
||||
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
|
||||
|
||||
expect do
|
||||
conversation.destroy!
|
||||
end.to change(Audited::Audit, :count).by(1)
|
||||
|
||||
audit = Audited::Audit.last
|
||||
expect(audit.auditable_type).to eq('Conversation')
|
||||
expect(audit.action).to eq('destroy')
|
||||
expect(audit.auditable_id).to eq(conversation.id)
|
||||
end
|
||||
|
||||
it 'does not create audit log for other actions by default' do
|
||||
skip 'Enterprise audit module not available' unless defined?(Enterprise::Audit::Conversation)
|
||||
|
||||
expect do
|
||||
conversation.update!(priority: 'high')
|
||||
end.not_to(change(Audited::Audit, :count))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,34 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ConversationPolicy, type: :policy do
|
||||
subject { described_class }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:administrator) { create(:user, account: account, role: :administrator) }
|
||||
let(:agent) { create(:user, account: account, role: :agent) }
|
||||
let(:administrator_context) { { user: administrator, account: account, account_user: administrator.account_users.first } }
|
||||
let(:agent_context) { { user: agent, account: account, account_user: agent.account_users.first } }
|
||||
|
||||
permissions :destroy? do
|
||||
context 'when user is an administrator' do
|
||||
it 'allows destroy' do
|
||||
expect(subject).to permit(administrator_context, conversation)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is an agent' do
|
||||
it 'denies destroy' do
|
||||
expect(subject).not_to permit(agent_context, conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :index? do
|
||||
context 'when user is authenticated' do
|
||||
it 'allows index' do
|
||||
expect(subject).to permit(agent_context, conversation)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CsatSurveyResponsePolicy, type: :policy do
|
||||
subject(:csat_policy) { described_class }
|
||||
|
||||
let(:account) { create(:account) }
|
||||
let(:administrator) { create(:user, :administrator, account: account) }
|
||||
let(:agent) { create(:user, account: account) }
|
||||
let(:csat_survey_response) { create(:csat_survey_response, account: account) }
|
||||
|
||||
let(:administrator_context) { { user: administrator, account: account, account_user: account.account_users.first } }
|
||||
let(:agent_context) { { user: agent, account: account, account_user: account.account_users.last } }
|
||||
|
||||
permissions :index?, :metrics?, :download? do
|
||||
context 'when administrator' do
|
||||
it { expect(csat_policy).to permit(administrator_context, csat_survey_response) }
|
||||
end
|
||||
|
||||
context 'when agent' do
|
||||
it { expect(csat_policy).not_to permit(agent_context, csat_survey_response) }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -121,9 +121,8 @@ describe MessageTemplates::HookExecutionService do
|
||||
create(:message, conversation: conversation, message_type: 'incoming')
|
||||
end
|
||||
|
||||
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled and can reply' do
|
||||
it 'calls ::MessageTemplates::Template::CsatSurvey when a conversation is resolved in an inbox with survey enabled' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
allow(conversation).to receive(:can_reply?).and_return(true)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
@@ -173,32 +172,6 @@ describe MessageTemplates::HookExecutionService do
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'will not call ::MessageTemplates::Template::CsatSurvey if cannot reply' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
allow(conversation).to receive(:can_reply?).and_return(false)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(MessageTemplates::Template::CsatSurvey).not_to have_received(:new).with(conversation: conversation)
|
||||
expect(csat_survey).not_to have_received(:perform)
|
||||
end
|
||||
|
||||
it 'creates activity message when CSAT not sent due to messaging window restriction' do
|
||||
conversation.inbox.update(csat_survey_enabled: true)
|
||||
allow(conversation).to receive(:can_reply?).and_return(false)
|
||||
allow(conversation).to receive(:create_csat_not_sent_activity_message)
|
||||
|
||||
conversation.resolved!
|
||||
Conversations::ActivityMessageJob.perform_now(conversation,
|
||||
{ account_id: conversation.account_id, inbox_id: conversation.inbox_id, message_type: :activity,
|
||||
content: 'Conversation marked resolved!!' })
|
||||
|
||||
expect(conversation).to have_received(:create_csat_not_sent_activity_message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when it is after working hours' do
|
||||
|
||||
@@ -156,33 +156,18 @@ describe SearchService do
|
||||
end
|
||||
|
||||
context 'when article search' do
|
||||
it 'orders results by updated_at desc' do
|
||||
# Create articles with explicit timestamps
|
||||
older_time = 2.days.ago
|
||||
newer_time = 1.hour.ago
|
||||
|
||||
it 'returns matching articles' do
|
||||
article2 = create(:article, title: 'Spellcasting Guide',
|
||||
account: account, portal: portal, author: user, status: 'published')
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
article2.update_column(:updated_at, older_time)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
article3 = create(:article, title: 'Spellcasting Manual',
|
||||
account: account, portal: portal, author: user, status: 'published')
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
article3.update_column(:updated_at, newer_time)
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
|
||||
params = { q: 'Spellcasting' }
|
||||
search = described_class.new(current_user: user, current_account: account, params: params, search_type: 'Article')
|
||||
results = search.perform[:articles]
|
||||
|
||||
# Check the timestamps to understand ordering
|
||||
results.map { |a| [a.id, a.updated_at] }
|
||||
|
||||
# Should be ordered by updated_at desc (newer first)
|
||||
expect(results.length).to eq(2)
|
||||
expect(results.first.updated_at).to be > results.second.updated_at
|
||||
expect(results.map(&:id)).to contain_exactly(article2.id, article3.id)
|
||||
end
|
||||
|
||||
it 'returns paginated results' do
|
||||
|
||||
Reference in New Issue
Block a user