trabajo-afectivo/spec/models/ticket/article_spec.rb

644 lines
23 KiB
Ruby
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
require 'rails_helper'
require 'models/application_model_examples'
require 'models/concerns/can_be_imported_examples'
require 'models/concerns/can_csv_import_examples'
require 'models/concerns/has_history_examples'
require 'models/concerns/has_object_manager_attributes_examples'
require 'models/ticket/article/has_ticket_contact_attributes_impact_examples'
RSpec.describe Ticket::Article, type: :model do
subject(:article) { create(:ticket_article) }
it_behaves_like 'ApplicationModel'
it_behaves_like 'CanBeImported'
it_behaves_like 'CanCsvImport'
it_behaves_like 'HasHistory'
it_behaves_like 'HasObjectManagerAttributes'
it_behaves_like 'Ticket::Article::HasTicketContactAttributesImpact'
describe 'Callbacks, Observers, & Async Transactions -' do
describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
it 'removes them from #subject on creation, if necessary (postgres doesnt like them)' do
expect(create(:ticket_article, subject: "com test 1\u0000"))
.to be_persisted
end
it 'removes them from #body on creation, if necessary (postgres doesnt like them)' do
expect(create(:ticket_article, body: "some\u0000message 123"))
.to be_persisted
end
end
describe 'Setting of ticket_define_email_from' do
subject(:article) do
create(:ticket_article, created_by: created_by, sender_name: 'Agent', type_name: 'email')
end
context 'when AgentName' do
before do
Setting.set('ticket_define_email_from', 'AgentName')
end
context 'with real sender' do
let(:created_by) { create(:user) }
it 'sets the from to the realname of the user' do
expect(article.reload.from).to eq("\"#{article.created_by.firstname} #{article.created_by.lastname}\" <#{article.ticket.group.email_address.email}>")
end
end
context 'with no real sender (e.g. trigger or scheduler)' do
let(:created_by) { User.find(1) }
it 'sets the from to realname of the mail address)' do
expect(article.reload.from).to eq("\"#{article.ticket.group.email_address.realname}\" <#{article.ticket.group.email_address.email}>")
end
end
end
end
describe 'Setting of ticket.create_article_{sender,type}' do
let!(:ticket) { create(:ticket) }
context 'on creation' do
context 'of first article on a ticket' do
subject(:article) do
create(:ticket_article, ticket: ticket, sender_name: 'Agent', type_name: 'email')
end
it 'sets ticket sender/type attributes based on article sender/type' do
expect { article }
.to change { ticket.reload.create_article_sender&.name }.to('Agent')
.and change { ticket.reload.create_article_type&.name }.to('email')
end
end
context 'of subsequent articles on a ticket' do
subject(:article) do
create(:ticket_article, ticket: ticket, sender_name: 'Customer', type_name: 'twitter status')
end
let!(:first_article) do
create(:ticket_article, ticket: ticket, sender_name: 'Agent', type_name: 'email')
end
it 'does not modify tickets sender/type attributes' do
expect { article }
.to not_change { ticket.reload.create_article_sender.name }
.and not_change { ticket.reload.create_article_type.name }
end
end
end
end
describe 'XSS protection:' do
subject(:article) { create(:ticket_article, body: body, content_type: 'text/html') }
before do
# XSS processing may run into a timeout on slow CI systems, so turn the timeout off for the test.
stub_const("#{HtmlSanitizer}::PROCESSING_TIMEOUT", nil)
end
context 'when body contains only injected JS' do
let(:body) { <<~RAW.chomp }
<script type="text/javascript">alert("XSS!");</script> some other text
RAW
it 'removes <script> tags' do
expect(article.body).to eq(' some other text')
end
end
context 'when body contains injected JS amidst other text' do
let(:body) { <<~RAW.chomp }
please tell me this doesn't work: <script type="text/javascript">alert("XSS!");</script>
RAW
it 'removes <script> tags' do
expect(article.body).to eq(<<~SANITIZED.chomp)
please tell me this doesn't work:#{' '}
SANITIZED
end
end
context 'when body contains invalid HTML tags' do
let(:body) { '<some_not_existing>ABC</some_not_existing>' }
it 'removes invalid tags' do
expect(article.body).to eq('ABC')
end
end
context 'when body contains restricted HTML attributes' do
let(:body) { '<div class="adasd" id="123" data-abc="123"></div>' }
it 'removes restricted attributes' do
expect(article.body).to eq('<div></div>')
end
end
context 'when body contains JS injected into href attribute' do
let(:body) { '<a href="javascript:someFunction()">LINK</a>' }
it 'removes <a> tags' do
expect(article.body).to eq('LINK')
end
end
context 'when body contains an unclosed <div> element' do
let(:body) { '<div>foo' }
it 'closes it' do
expect(article.body).to eq('<div>foo</div>')
end
end
context 'when body contains a plain link (<a> element)' do
let(:body) { '<a href="https://example.com">foo</a>' }
it 'adds sanitization attributes' do
expect(article.body).to eq(<<~SANITIZED.chomp)
<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">foo</a>
SANITIZED
end
context 'when a sanitization attribute is present' do
# ATTENTION: We use `target` here because re-sanitization would change the order of attributes
let(:body) { '<a href="https://example.com" target="_blank">foo</a>' }
it 'adds sanitization attributes' do
expect(article.body).to eq(<<~SANITIZED.chomp)
<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">foo</a>
SANITIZED
end
context 'when changing an unrelated attribute' do
it "doesn't re-sanitizes the body" do
expect { article.update!(message_id: 'test') }.not_to change { article.reload.body }
end
end
end
end
context 'for all cases above, combined' do
let(:body) { <<~RAW.chomp }
please tell me this doesn't work: <table>ada<tr></tr></table>
<div class="adasd" id="123" data-abc="123"></div>
<div>
<a href="javascript:someFunction()">LINK</a>
<a href="http://lalal.de">aa</a>
<some_not_existing>ABC</some_not_existing>
RAW
it 'performs all sanitizations' do
expect(article.body).to eq(<<~SANITIZED.chomp)
please tell me this doesn't work: <table>ada<tr></tr>
</table>
<div></div>
<div>
LINK
<a href="http://lalal.de" rel="nofollow noreferrer noopener" target="_blank" title="http://lalal.de">aa</a>
ABC
</div>
SANITIZED
end
end
context 'for content_type: "text/plain"' do
subject(:article) { create(:ticket_article, body: body, content_type: 'text/plain') }
let(:body) { <<~RAW.chomp }
please tell me this doesn't work: <table>ada<tr></tr></table>
<div class="adasd" id="123" data-abc="123"></div>
<div>
<a href="javascript:someFunction()">LINK</a>
<a href="http://lalal.de">aa</a>
<some_not_existing>ABC</some_not_existing>
RAW
it 'performs no sanitizations' do
expect(article.body).to eq(<<~SANITIZED.chomp)
please tell me this doesn't work: <table>ada<tr></tr></table>
<div class="adasd" id="123" data-abc="123"></div>
<div>
<a href="javascript:someFunction()">LINK</a>
<a href="http://lalal.de">aa</a>
<some_not_existing>ABC</some_not_existing>
SANITIZED
end
end
context 'when body contains <video> element' do
let(:body) { <<~RAW.chomp }
please tell me this doesn't work: <video>some video</video><foo>alal</foo>
RAW
it 'leaves it as-is' do
expect(article.body).to eq(<<~SANITIZED.chomp)
please tell me this doesn't work: <video>some video</video>alal
SANITIZED
end
end
context 'when body contains CSS in style attribute' do
context 'for cid-style email attachment' do
let(:body) { <<~RAW.chomp }
<img style="width: 85.5px; height: 49.5px" src="cid:15.274327094.140938@zammad.example.com">
asdasd
<img src="cid:15.274327094.140939@zammad.example.com">
RAW
it 'adds terminal semicolons to style rules' do
expect(article.body).to eq(<<~SANITIZED.chomp)
<img style="width: 85.5px; height: 49.5px;" src="cid:15.274327094.140938@zammad.example.com">
asdasd
<img src="cid:15.274327094.140939@zammad.example.com">
SANITIZED
end
end
context 'for relative-path-style email attachment' do
let(:body) { <<~RAW.chomp }
<img style="width: 85.5px; height: 49.5px" src="api/v1/ticket_attachment/123/123/123">
asdasd
<img src="api/v1/ticket_attachment/123/123/123">
RAW
it 'adds terminal semicolons to style rules' do
expect(article.body).to eq(<<~SANITIZED.chomp)
<img style="width: 85.5px; height: 49.5px;" src="api/v1/ticket_attachment/123/123/123">
asdasd
<img src="api/v1/ticket_attachment/123/123/123">
SANITIZED
end
end
end
context 'when body contains <body> elements' do
let(:body) { '<body>123</body>' }
it 'removes <body> tags' do
expect(article.body).to eq('123')
end
end
context 'when body contains onclick attributes in <a> elements' do
let(:body) { <<~RAW.chomp }
<a href="#" onclick="some_function();">abc</a>
<a href="https://example.com" oNclIck="some_function();">123</a>
RAW
it 'removes onclick attributes' do
expect(article.body).to eq(<<~SANITIZED.chomp)
<a href="#">abc</a>
<a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">123</a>
SANITIZED
end
end
end
describe 'DoS protection:' do
context 'when #body exceeds 1.5MB' do
subject(:article) { create(:ticket_article, body: body) }
let(:body) { 'a' * 2_000_000 }
context 'for "web" thread', application_handle: 'web' do
it 'raises an Unprocessable Entity error' do
expect { article }.to raise_error(Exceptions::UnprocessableEntity)
end
end
context 'for import' do
before do
Setting.set('import_mode', true)
end
it 'truncates body to 1.5 million chars' do
expect(article.body.length).to eq(1_500_000)
end
end
context 'for "test.postmaster" thread', application_handle: 'test.postmaster' do
it 'truncates body to 1.5 million chars' do
expect(article.body.length).to eq(1_500_000)
end
context 'with NULL bytes' do
let(:body) { "\u0000#{'a' * 2_000_000}" }
it 'still removes them, if necessary (postgres doesnt like them)' do
expect(article).to be_persisted
end
it 'still truncates body' do
expect(article.body.length).to eq(1_500_000)
end
end
end
end
end
describe 'Cti::Log syncing:' do
context 'with existing Log records' do
context 'for an incoming call from an unknown number' do
let!(:log) { create(:'cti/log', :with_preferences, from: '491111222222', direction: 'in') }
context 'with that number in #body' do
subject(:article) { build(:ticket_article, body: <<~BODY) }
some message
+49 1111 222222
BODY
it 'does not modify any Log records (because CallerIds from article bodies have #level "maybe")' do
expect do
article.save
TransactionDispatcher.commit
Scheduler.worker(true)
end.not_to change { log.reload.attributes }
end
end
end
end
end
describe 'Auto-setting of outgoing Twitter article attributes (via bg jobs):', use_vcr: :with_oauth_headers, required_envs: %w[TWITTER_CONSUMER_KEY TWITTER_CONSUMER_SECRET TWITTER_OAUTH_TOKEN TWITTER_OAUTH_TOKEN_SECRET] do
subject!(:twitter_article) { create(:twitter_article, sender_name: 'Agent') }
let(:channel) { Channel.find(twitter_article.ticket.preferences[:channel_id]) }
let(:run_bg_jobs) { -> { Scheduler.worker(true) } }
it 'sets #from to senders Twitter handle' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.from }
.to('@ZammadTesting')
end
it 'sets #to to recipients Twitter handle' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.to }
.to('') # Tweet in VCR cassette is addressed to no one
end
it 'sets #message_id to tweet ID (https://twitter.com/_/status/<id>)' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.message_id }
.to('1410130368498372609')
end
it 'sets #preferences with tweet metadata' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.preferences }
.to(hash_including('twitter', 'links'))
expect(twitter_article.preferences[:links].first)
.to include(
'name' => 'on Twitter',
'target' => '_blank',
'url' => "https://twitter.com/_/status/#{twitter_article.message_id}"
)
end
it 'does not change #cc' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.cc }
end
it 'does not change #subject' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.subject }
end
it 'does not change #content_type' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.content_type }
end
it 'does not change #body' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.body }
end
it 'does not change #sender' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.sender }
end
it 'does not change #type' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.type }
end
it 'sets appropriate status attributes on the tickets channel' do
expect(&run_bg_jobs)
.to change { channel.reload.attributes }
.to hash_including(
'status_in' => nil,
'last_log_in' => nil,
'status_out' => 'ok',
'last_log_out' => ''
)
end
context 'when the original channel (specified in ticket.preferences) was deleted' do
context 'but a new one with the same screen_name exists' do
let(:new_channel) { create(:twitter_channel) }
before do
channel.destroy
expect(new_channel.options[:user][:screen_name])
.to eq(channel.options[:user][:screen_name])
end
it 'sets appropriate status attributes on the new channel' do
expect(&run_bg_jobs)
.to change { new_channel.reload.attributes }
.to hash_including(
'status_in' => nil,
'last_log_in' => nil,
'status_out' => 'ok',
'last_log_out' => ''
)
end
end
end
end
describe 'Sending of outgoing emails', performs_jobs: true do
subject(:article) { create(:ticket_article, type_name: type, sender_name: sender) }
shared_examples 'sends email' do
it 'dispatches an email on creation (via TicketArticleCommunicateEmailJob)' do
expect { article }
.to have_enqueued_job(TicketArticleCommunicateEmailJob)
end
end
shared_examples 'does not send email' do
it 'does not dispatch an email' do
expect { article }
.not_to have_enqueued_job(TicketArticleCommunicateEmailJob)
end
end
context 'with "application_server" application handle', application_handle: 'application_server' do
context 'for type: "email"' do
let(:type) { 'email' }
context 'from sender: "Agent"' do
let(:sender) { 'Agent' }
include_examples 'sends email'
end
context 'from sender: "Customer"' do
let(:sender) { 'Customer' }
include_examples 'does not send email'
end
end
context 'for any other type' do
let(:type) { 'sms' }
context 'from any sender' do
let(:sender) { 'Agent' }
include_examples 'does not send email'
end
end
end
context 'with "*.postmaster" application handle', application_handle: 'scheduler.postmaster' do
context 'for any type' do
let(:type) { 'email' }
context 'from any sender' do
let(:sender) { 'Agent' }
include_examples 'does not send email'
end
end
end
end
end
describe 'clone attachments' do
context 'of forwarded article' do
context 'via email' do
it 'only need to clone attached attachments' do
article_parent = create(:ticket_article,
type: Ticket::Article::Type.find_by(name: 'email'),
content_type: 'text/html',
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file1_normally_should_be_an_image',
filename: 'some_file1.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file2_normally_should_be_an_image',
filename: 'some_file2.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file3_normally_should_be_an_image',
filename: 'some_file3.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-Disposition' => 'attached',
},
created_by_id: 1,
)
article_new = create(:ticket_article)
UserInfo.current_user_id = 1
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
expect(attachments.count).to eq(2)
expect(attachments[0].filename).to eq('some_file2.jpg')
expect(attachments[1].filename).to eq('some_file3.jpg')
end
end
end
context 'of trigger' do
context 'via email notifications' do
it 'only need to clone inline attachments used in body' do
article_parent = create(:ticket_article,
type: Ticket::Article::Type.find_by(name: 'email'),
content_type: 'text/html',
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file1_normally_should_be_an_image',
filename: 'some_file1.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file2_normally_should_be_an_image',
filename: 'some_file2.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
# #2483 - #{article.body_as_html} now includes attachments (e.g. PDFs)
# Regular attachments do not get assigned a Content-ID, and should not be copied in this use case
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file3_with_no_content_id',
filename: 'some_file3.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
},
created_by_id: 1,
)
article_new = create(:ticket_article)
UserInfo.current_user_id = 1
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
expect(attachments.count).to eq(1)
expect(attachments[0].filename).to eq('some_file1.jpg')
end
end
end
end
end