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

585 lines
20 KiB
Ruby
Raw Normal View History

2018-12-13 09:06:44 +00:00
require 'rails_helper'
require 'models/application_model_examples'
require 'models/concerns/can_be_imported_examples'
require 'models/concerns/has_history_examples'
require 'models/concerns/has_object_manager_attributes_validation_examples'
RSpec.describe Ticket::Article, type: :model do
it_behaves_like 'ApplicationModel'
it_behaves_like 'CanBeImported'
it_behaves_like 'HasHistory'
it_behaves_like 'HasObjectManagerAttributesValidation'
2018-12-13 09:06:44 +00:00
subject(:article) { create(:ticket_article) }
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.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') }
context 'when body contains only injected JS' do
let(:body) { <<~RAW.chomp }
<script type="text/javascript">alert("XSS!");</script>
RAW
it 'removes <script> tags' do
expect(article.body).to eq('alert("XSS!");')
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: alert("XSS!");
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
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 "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
Observer::Transaction.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):' do
subject!(:twitter_article) { create(:twitter_article, sender_name: 'Agent') }
let(:channel) { Channel.find(twitter_article.ticket.preferences[:channel_id]) }
let(:run_bg_jobs) do
lambda do
VCR.use_cassette(cassette, match_requests_on: %i[method uri oauth_headers]) do
Scheduler.worker(true)
2018-12-13 09:06:44 +00:00
end
end
end
2018-12-13 09:06:44 +00:00
let(:cassette) { 'models/channel/driver/twitter/article_to_tweet' }
2018-12-13 09:06:44 +00:00
it 'sets #from to senders Twitter handle' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.from }
.to('@example')
end
2018-12-13 09:06:44 +00:00
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
2018-12-13 09:06:44 +00:00
it 'sets #message_id to tweet ID (https://twitter.com/statuses/<id>)' do
expect(&run_bg_jobs)
.to change { twitter_article.reload.message_id }
.to('1069382411899817990')
end
2018-12-13 09:06:44 +00:00
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/statuses/#{twitter_article.message_id}"
)
end
2018-12-13 09:06:44 +00:00
it 'does not change #cc' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.cc }
end
2018-12-13 09:06:44 +00:00
it 'does not change #subject' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.subject }
end
2018-12-13 09:06:44 +00:00
it 'does not change #content_type' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.content_type }
end
2018-12-13 09:06:44 +00:00
it 'does not change #body' do
expect(&run_bg_jobs).not_to change { twitter_article.reload.body }
end
2018-12-13 09:06:44 +00:00
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
2018-12-13 09:06:44 +00:00
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(:cassette) { 'models/channel/driver/twitter/article_to_tweet_channel_replace' }
let(:new_channel) { create(:twitter_channel) }
before do
channel.destroy
expect(new_channel.options[:user][:screen_name])
.to eq(channel.options[:user][:screen_name])
2018-12-13 09:06:44 +00:00
end
it 'sets appropriate status attributes on the new channel' do
2018-12-13 09:06:44 +00:00
expect(&run_bg_jobs)
.to change { new_channel.reload.attributes }
2018-12-13 09:06:44 +00:00
.to hash_including(
'status_in' => nil,
'last_log_in' => nil,
'status_out' => 'ok',
'last_log_out' => ''
2018-12-13 09:06:44 +00:00
)
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
2018-12-13 09:06:44 +00:00
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
2018-12-13 09:06:44 +00:00
end