article_spec.rb 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. require 'models/application_model_examples'
  4. require 'models/concerns/can_be_imported_examples'
  5. require 'models/concerns/can_csv_import_examples'
  6. require 'models/concerns/has_history_examples'
  7. require 'models/concerns/has_object_manager_attributes_examples'
  8. require 'models/ticket/article/has_ticket_contact_attributes_impact_examples'
  9. RSpec.describe Ticket::Article, type: :model do
  10. subject(:article) { create(:ticket_article) }
  11. it_behaves_like 'ApplicationModel', can_param: { sample_data_attribute: :body }
  12. it_behaves_like 'CanBeImported'
  13. it_behaves_like 'CanCsvImport'
  14. it_behaves_like 'HasHistory'
  15. it_behaves_like 'HasObjectManagerAttributes'
  16. it_behaves_like 'Ticket::Article::HasTicketContactAttributesImpact'
  17. describe 'Callbacks, Observers, & Async Transactions -' do
  18. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  19. it 'removes them from #subject on creation, if necessary (postgres doesn’t like them)' do
  20. expect(create(:ticket_article, subject: "com test 1\u0000"))
  21. .to be_persisted
  22. end
  23. it 'removes them from #body on creation, if necessary (postgres doesn’t like them)' do
  24. expect(create(:ticket_article, body: "some\u0000message 123"))
  25. .to be_persisted
  26. end
  27. end
  28. describe 'Setting of ticket_define_email_from' do
  29. subject(:article) do
  30. create(:ticket_article, created_by: created_by, sender_name: 'Agent', type_name: 'email')
  31. end
  32. context 'when AgentName' do
  33. before do
  34. Setting.set('ticket_define_email_from', 'AgentName')
  35. end
  36. context 'with real sender' do
  37. let(:created_by) { create(:user) }
  38. it 'sets the from to the realname of the user' do
  39. expect(article.reload.from).to be_in(
  40. [
  41. "#{article.created_by.firstname} #{article.created_by.lastname} <#{article.ticket.group.email_address.email}>",
  42. "\"#{article.created_by.firstname} #{article.created_by.lastname}\" <#{article.ticket.group.email_address.email}>",
  43. ]
  44. )
  45. end
  46. end
  47. context 'with no real sender (e.g. trigger or scheduler)' do
  48. let(:created_by) { User.find(1) }
  49. it 'sets the from to realname of the mail address)' do
  50. expect(article.reload.from).to eq("#{article.ticket.group.email_address.name} <#{article.ticket.group.email_address.email}>")
  51. end
  52. end
  53. end
  54. end
  55. describe 'Setting of ticket.create_article_{sender,type}' do
  56. let!(:ticket) { create(:ticket) }
  57. context 'on creation' do
  58. context 'of first article on a ticket' do
  59. subject(:article) do
  60. create(:ticket_article, ticket: ticket, sender_name: 'Agent', type_name: 'email')
  61. end
  62. it 'sets ticket sender/type attributes based on article sender/type' do
  63. expect { article }
  64. .to change { ticket.reload.create_article_sender&.name }.to('Agent')
  65. .and change { ticket.reload.create_article_type&.name }.to('email')
  66. end
  67. end
  68. context 'of subsequent articles on a ticket' do
  69. subject(:article) do
  70. create(:ticket_article, ticket: ticket, sender_name: 'Customer', type_name: 'twitter status')
  71. end
  72. let!(:first_article) do
  73. create(:ticket_article, ticket: ticket, sender_name: 'Agent', type_name: 'email')
  74. end
  75. it 'does not modify ticket’s sender/type attributes' do
  76. expect { article }
  77. .to not_change { ticket.reload.create_article_sender.name }
  78. .and not_change { ticket.reload.create_article_type.name }
  79. end
  80. end
  81. end
  82. end
  83. describe 'XSS protection:' do
  84. subject(:article) { create(:ticket_article, body: body, content_type: 'text/html') }
  85. before do
  86. # XSS processing may run into a timeout on slow CI systems, so turn the timeout off for the test.
  87. stub_const("#{HtmlSanitizer}::PROCESSING_TIMEOUT", nil)
  88. end
  89. context 'when body contains only injected JS' do
  90. let(:body) { <<~RAW.chomp }
  91. <script type="text/javascript">alert("XSS!");</script> some other text
  92. RAW
  93. it 'removes <script> tags' do
  94. expect(article.body).to eq(' some other text')
  95. end
  96. end
  97. context 'when body contains injected JS amidst other text' do
  98. let(:body) { <<~RAW.chomp }
  99. please tell me this doesn't work: <script type="text/javascript">alert("XSS!");</script>
  100. RAW
  101. it 'removes <script> tags' do
  102. expect(article.body).to eq(<<~SANITIZED.chomp)
  103. please tell me this doesn't work:#{' '}
  104. SANITIZED
  105. end
  106. end
  107. context 'when body contains invalid HTML tags' do
  108. let(:body) { '<some_not_existing>ABC</some_not_existing>' }
  109. it 'removes invalid tags' do
  110. expect(article.body).to eq('ABC')
  111. end
  112. end
  113. context 'when body contains restricted HTML attributes' do
  114. let(:body) { '<div class="adasd" id="123" data-abc="123"></div>' }
  115. it 'removes restricted attributes' do
  116. expect(article.body).to eq('<div></div>')
  117. end
  118. end
  119. context 'when body contains JS injected into href attribute' do
  120. let(:body) { '<a href="javascript:someFunction()">LINK</a>' }
  121. it 'removes <a> tags' do
  122. expect(article.body).to eq('LINK')
  123. end
  124. end
  125. context 'when body contains an unclosed <div> element' do
  126. let(:body) { '<div>foo' }
  127. it 'closes it' do
  128. expect(article.body).to eq('<div>foo</div>')
  129. end
  130. end
  131. context 'when body contains a plain link (<a> element)' do
  132. let(:body) { '<a href="https://example.com">foo</a>' }
  133. it 'adds sanitization attributes' do
  134. expect(article.body).to eq(<<~SANITIZED.chomp)
  135. <a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">foo</a>
  136. SANITIZED
  137. end
  138. context 'when a sanitization attribute is present' do
  139. # ATTENTION: We use `target` here because re-sanitization would change the order of attributes
  140. let(:body) { '<a href="https://example.com" target="_blank">foo</a>' }
  141. it 'adds sanitization attributes' do
  142. expect(article.body).to eq(<<~SANITIZED.chomp)
  143. <a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">foo</a>
  144. SANITIZED
  145. end
  146. context 'when changing an unrelated attribute' do
  147. it "doesn't re-sanitizes the body" do
  148. expect { article.update!(message_id: 'test') }.not_to change { article.reload.body }
  149. end
  150. end
  151. end
  152. end
  153. context 'for all cases above, combined' do
  154. let(:body) { <<~RAW.chomp }
  155. please tell me this doesn't work: <table>ada<tr></tr></table>
  156. <div class="adasd" id="123" data-abc="123"></div>
  157. <div>
  158. <a href="javascript:someFunction()">LINK</a>
  159. <a href="http://lalal.de">aa</a>
  160. <some_not_existing>ABC</some_not_existing>
  161. RAW
  162. it 'performs all sanitizations' do
  163. expect(article.body).to eq(<<~SANITIZED.chomp)
  164. please tell me this doesn't work: <table>ada<tr></tr>
  165. </table>
  166. <div></div>
  167. <div>
  168. LINK
  169. <a href="http://lalal.de" rel="nofollow noreferrer noopener" target="_blank" title="http://lalal.de">aa</a>
  170. ABC
  171. </div>
  172. SANITIZED
  173. end
  174. end
  175. context 'for content_type: "text/plain"' do
  176. subject(:article) { create(:ticket_article, body: body, content_type: 'text/plain') }
  177. let(:body) { <<~RAW.chomp }
  178. please tell me this doesn't work: <table>ada<tr></tr></table>
  179. <div class="adasd" id="123" data-abc="123"></div>
  180. <div>
  181. <a href="javascript:someFunction()">LINK</a>
  182. <a href="http://lalal.de">aa</a>
  183. <some_not_existing>ABC</some_not_existing>
  184. RAW
  185. it 'performs no sanitizations' do
  186. expect(article.body).to eq(<<~SANITIZED.chomp)
  187. please tell me this doesn't work: <table>ada<tr></tr></table>
  188. <div class="adasd" id="123" data-abc="123"></div>
  189. <div>
  190. <a href="javascript:someFunction()">LINK</a>
  191. <a href="http://lalal.de">aa</a>
  192. <some_not_existing>ABC</some_not_existing>
  193. SANITIZED
  194. end
  195. end
  196. context 'when body contains <video> element' do
  197. let(:body) { <<~RAW.chomp }
  198. please tell me this doesn't work: <video>some video</video><foo>alal</foo>
  199. RAW
  200. it 'leaves it as-is' do
  201. expect(article.body).to eq(<<~SANITIZED.chomp)
  202. please tell me this doesn't work: <video>some video</video>alal
  203. SANITIZED
  204. end
  205. end
  206. context 'when body contains CSS in style attribute' do
  207. context 'for cid-style email attachment' do
  208. let(:body) { <<~RAW.chomp }
  209. <img style="width: 85.5px; height: 49.5px" src="cid:15.274327094.140938@zammad.example.com">
  210. asdasd
  211. <img src="cid:15.274327094.140939@zammad.example.com">
  212. RAW
  213. it 'adds terminal semicolons to style rules' do
  214. expect(article.body).to eq(<<~SANITIZED.chomp)
  215. <img style="width: 85.5px; height: 49.5px;" src="cid:15.274327094.140938@zammad.example.com">
  216. asdasd
  217. <img src="cid:15.274327094.140939@zammad.example.com">
  218. SANITIZED
  219. end
  220. end
  221. context 'for relative-path-style email attachment' do
  222. let(:body) { <<~RAW.chomp }
  223. <img style="width: 85.5px; height: 49.5px" src="api/v1/ticket_attachment/123/123/123">
  224. asdasd
  225. <img src="api/v1/ticket_attachment/123/123/123">
  226. RAW
  227. it 'adds terminal semicolons to style rules' do
  228. expect(article.body).to eq(<<~SANITIZED.chomp)
  229. <img style="width: 85.5px; height: 49.5px;" src="api/v1/ticket_attachment/123/123/123">
  230. asdasd
  231. <img src="api/v1/ticket_attachment/123/123/123">
  232. SANITIZED
  233. end
  234. end
  235. end
  236. context 'when body contains <body> elements' do
  237. let(:body) { '<body>123</body>' }
  238. it 'removes <body> tags' do
  239. expect(article.body).to eq('123')
  240. end
  241. end
  242. context 'when body contains onclick attributes in <a> elements' do
  243. let(:body) { <<~RAW.chomp }
  244. <a href="#" onclick="some_function();">abc</a>
  245. <a href="https://example.com" oNclIck="some_function();">123</a>
  246. RAW
  247. it 'removes onclick attributes' do
  248. expect(article.body).to eq(<<~SANITIZED.chomp)
  249. <a href="#">abc</a>
  250. <a href="https://example.com" rel="nofollow noreferrer noopener" target="_blank" title="https://example.com">123</a>
  251. SANITIZED
  252. end
  253. end
  254. end
  255. describe 'DoS protection:' do
  256. context 'when #body exceeds 1.5MB' do
  257. subject(:article) { create(:ticket_article, body: body) }
  258. let(:body) { 'a' * 2_000_000 }
  259. context 'for "web" thread', application_handle: 'web' do
  260. it 'raises an Unprocessable Entity error' do
  261. expect { article }.to raise_error(Exceptions::UnprocessableEntity)
  262. end
  263. end
  264. context 'for import' do
  265. before do
  266. Setting.set('import_mode', true)
  267. end
  268. it 'truncates body to 1.5 million chars' do
  269. expect(article.body.length).to eq(1_500_000)
  270. end
  271. end
  272. context 'for "test.postmaster" thread', application_handle: 'test.postmaster' do
  273. it 'truncates body to 1.5 million chars' do
  274. expect(article.body.length).to eq(1_500_000)
  275. end
  276. context 'with NULL bytes' do
  277. let(:body) { "\u0000#{'a' * 2_000_000}" }
  278. it 'still removes them, if necessary (postgres doesn’t like them)' do
  279. expect(article).to be_persisted
  280. end
  281. it 'still truncates body' do
  282. expect(article.body.length).to eq(1_500_000)
  283. end
  284. end
  285. end
  286. end
  287. end
  288. describe 'Cti::Log syncing:', performs_jobs: true do
  289. context 'with existing Log records' do
  290. context 'for an incoming call from an unknown number' do
  291. let!(:log) { create(:'cti/log', :with_preferences, from: '491111222222', direction: 'in') }
  292. context 'with that number in #body' do
  293. subject(:article) { build(:ticket_article, body: <<~BODY) }
  294. some message
  295. +49 1111 222222
  296. BODY
  297. it 'does not modify any Log records (because CallerIds from article bodies have #level "maybe")' do
  298. expect do
  299. article.save
  300. perform_enqueued_jobs commit_transaction: true
  301. end.not_to change { log.reload.attributes }
  302. end
  303. end
  304. end
  305. end
  306. end
  307. describe 'Auto-setting of outgoing Twitter article attributes (via bg jobs):', performs_jobs: true, required_envs: %w[TWITTER_CONSUMER_KEY TWITTER_CONSUMER_SECRET TWITTER_OAUTH_TOKEN TWITTER_OAUTH_TOKEN_SECRET], use_vcr: :with_oauth_headers do
  308. subject!(:twitter_article) { create(:twitter_article, sender_name: 'Agent') }
  309. let(:channel) { Channel.find(twitter_article.ticket.preferences[:channel_id]) }
  310. it 'sets #from to sender’s Twitter handle' do
  311. expect { perform_enqueued_jobs }
  312. .to change { twitter_article.reload.from }
  313. .to('@APITesting001')
  314. end
  315. it 'sets #to to recipient’s Twitter handle' do
  316. expect { perform_enqueued_jobs }
  317. .to change { twitter_article.reload.to }
  318. .to('') # Tweet in VCR cassette is addressed to no one
  319. end
  320. it 'sets #message_id to tweet ID (https://twitter.com/_/status/<id>)' do
  321. expect { perform_enqueued_jobs }
  322. .to change { twitter_article.reload.message_id }
  323. end
  324. it 'sets #preferences with tweet metadata' do
  325. expect { perform_enqueued_jobs }
  326. .to change { twitter_article.reload.preferences }
  327. .to(hash_including('twitter', 'links'))
  328. expect(twitter_article.preferences[:links].first)
  329. .to include(
  330. 'name' => 'on Twitter',
  331. 'target' => '_blank',
  332. 'url' => "https://twitter.com/_/status/#{twitter_article.message_id}"
  333. )
  334. end
  335. it 'does not change #cc' do
  336. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.cc }
  337. end
  338. it 'does not change #subject' do
  339. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.subject }
  340. end
  341. it 'does not change #content_type' do
  342. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.content_type }
  343. end
  344. it 'does not change #body' do
  345. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.body }
  346. end
  347. it 'does not change #sender' do
  348. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.sender }
  349. end
  350. it 'does not change #type' do
  351. expect { perform_enqueued_jobs }.not_to change { twitter_article.reload.type }
  352. end
  353. it 'sets appropriate status attributes on the ticket’s channel' do
  354. expect { perform_enqueued_jobs }
  355. .to change { channel.reload.attributes }
  356. .to hash_including(
  357. 'status_in' => nil,
  358. 'last_log_in' => nil,
  359. 'status_out' => 'ok',
  360. 'last_log_out' => ''
  361. )
  362. end
  363. context 'when the original channel (specified in ticket.preferences) was deleted' do
  364. context 'but a new one with the same screen_name exists' do
  365. let(:new_channel) { create(:twitter_channel) }
  366. before do
  367. channel.destroy
  368. expect(new_channel.options[:user][:screen_name])
  369. .to eq(channel.options[:user][:screen_name])
  370. end
  371. it 'sets appropriate status attributes on the new channel' do
  372. expect { perform_enqueued_jobs }
  373. .to change { new_channel.reload.attributes }
  374. .to hash_including(
  375. 'status_in' => nil,
  376. 'last_log_in' => nil,
  377. 'status_out' => 'ok',
  378. 'last_log_out' => ''
  379. )
  380. end
  381. end
  382. end
  383. end
  384. describe 'Sending of outgoing emails', performs_jobs: true do
  385. subject(:article) { create(:ticket_article, type_name: type, sender_name: sender) }
  386. shared_examples 'sends email' do
  387. it 'dispatches an email on creation (via TicketArticleCommunicateEmailJob)' do
  388. expect { article }
  389. .to have_enqueued_job(TicketArticleCommunicateEmailJob)
  390. end
  391. end
  392. shared_examples 'does not send email' do
  393. it 'does not dispatch an email' do
  394. expect { article }
  395. .not_to have_enqueued_job(TicketArticleCommunicateEmailJob)
  396. end
  397. end
  398. context 'with "application_server" application handle', application_handle: 'application_server' do
  399. context 'for type: "email"' do
  400. let(:type) { 'email' }
  401. context 'from sender: "Agent"' do
  402. let(:sender) { 'Agent' }
  403. include_examples 'sends email'
  404. end
  405. context 'from sender: "Customer"' do
  406. let(:sender) { 'Customer' }
  407. include_examples 'does not send email'
  408. end
  409. end
  410. context 'for any other type' do
  411. let(:type) { 'sms' }
  412. context 'from any sender' do
  413. let(:sender) { 'Agent' }
  414. include_examples 'does not send email'
  415. end
  416. end
  417. end
  418. context 'with "*.postmaster" application handle', application_handle: 'scheduler.postmaster' do
  419. context 'for any type' do
  420. let(:type) { 'email' }
  421. context 'from any sender' do
  422. let(:sender) { 'Agent' }
  423. include_examples 'does not send email'
  424. end
  425. end
  426. end
  427. end
  428. describe '#check_mentions' do
  429. def text_blob_with(user)
  430. "Lorem ipsum dolor <a data-mention-user-id='#{user.id}'>#{user.fullname}</a>"
  431. end
  432. let(:ticket) { create(:ticket) }
  433. let(:agent_with_access) { create(:agent, groups: [ticket.group]) }
  434. let(:user_without_access) { create(:agent) }
  435. let(:passing_body) { text_blob_with(agent_with_access) }
  436. let(:failing_body) { text_blob_with(user_without_access) }
  437. let(:partial_body) { text_blob_with(user_without_access) + text_blob_with(agent_with_access) }
  438. context 'when created in email parsing' do
  439. before { ApplicationHandleInfo.current = 'postmaster' }
  440. it 'silently ignores mentions if agent cannot mention users' do
  441. UserInfo.current_user_id = user_without_access.id
  442. record = create(:ticket_article, body: passing_body)
  443. expect(record).to be_persisted
  444. expect(Mention.count).to be_zero
  445. end
  446. it 'silently ignores mentions if given users cannot be mentioned' do
  447. UserInfo.current_user_id = agent_with_access.id
  448. article = build(:ticket_article, ticket: ticket, body: failing_body)
  449. article.save
  450. expect(article).to be_persisted
  451. expect(Mention.count).to eq(0)
  452. end
  453. it 'silently saves passing user while failing user is skipped' do
  454. UserInfo.current_user_id = agent_with_access.id
  455. article = create(:ticket_article, ticket: ticket, body: partial_body)
  456. expect(article).to be_persisted
  457. expect(Mention.count).to eq(1)
  458. end
  459. it 'mentioned user is added' do
  460. UserInfo.current_user_id = agent_with_access.id
  461. create(:ticket_article, ticket: ticket, body: passing_body)
  462. expect(article).to be_persisted
  463. expect(Mention.count).to eq(1)
  464. end
  465. end
  466. context 'when created with check_mentions_raises_error set to true' do
  467. it 'raises an error if agent cannot mention users' do
  468. UserInfo.current_user_id = create(:customer).id
  469. article = build(:ticket_article, ticket: ticket, body: passing_body)
  470. article.check_mentions_raises_error = true
  471. expect { article.save! }
  472. .to raise_error(Pundit::NotAuthorizedError)
  473. expect(Mention.count).to eq(0)
  474. end
  475. it 'raises an error if given users cannot be mentioned' do
  476. UserInfo.current_user_id = agent_with_access.id
  477. article = build(:ticket_article, ticket: ticket, body: failing_body)
  478. article.check_mentions_raises_error = true
  479. expect { article.save! }
  480. .to raise_error(ActiveRecord::RecordInvalid)
  481. expect(Mention.count).to eq(0)
  482. end
  483. it 'raises an error if one if given users cannot be mentioned' do
  484. UserInfo.current_user_id = agent_with_access.id
  485. article = build(:ticket_article, ticket: ticket, body: partial_body)
  486. article.check_mentions_raises_error = true
  487. expect { article.save! }
  488. .to raise_error(ActiveRecord::RecordInvalid)
  489. expect(Mention.count).to eq(0)
  490. end
  491. it 'mentioned user is added' do
  492. UserInfo.current_user_id = agent_with_access.id
  493. article = build(:ticket_article, ticket: ticket, body: passing_body)
  494. article.check_mentions_raises_error = true
  495. article.save!
  496. expect(article).to be_persisted
  497. expect(Mention.count).to eq(1)
  498. end
  499. end
  500. context 'when created with check_mentions_raises_error set to false' do
  501. it 'silently ignores mentions if agent cannot mention users' do
  502. UserInfo.current_user_id = create(:customer).id
  503. article = build(:ticket_article, ticket: ticket, body: failing_body)
  504. article.save
  505. expect(article).to be_persisted
  506. expect(Mention.count).to eq(0)
  507. end
  508. it 'silently ignores mentions if given users cannot be mentioned' do
  509. UserInfo.current_user_id = agent_with_access.id
  510. article = build(:ticket_article, ticket: ticket, body: failing_body)
  511. article.save
  512. expect(article).to be_persisted
  513. expect(Mention.count).to eq(0)
  514. end
  515. it 'silently saves passing user while failing user is skipped' do
  516. UserInfo.current_user_id = agent_with_access.id
  517. article = create(:ticket_article, ticket: ticket, body: partial_body)
  518. expect(article).to be_persisted
  519. expect(Mention.count).to eq(1)
  520. end
  521. it 'mentioned user is added' do
  522. UserInfo.current_user_id = agent_with_access.id
  523. create(:ticket_article, ticket: ticket, body: passing_body)
  524. expect(article).to be_persisted
  525. expect(Mention.count).to eq(1)
  526. end
  527. end
  528. end
  529. describe '#check_email_recipient_validity' do
  530. subject(:article) do
  531. create(:ticket_article, type_name: type, to: to, check_email_recipient_raises_error: validate)
  532. end
  533. let(:type) { 'email' }
  534. let(:to) { nil }
  535. let(:validate) { false }
  536. shared_examples 'not raising an error' do
  537. it 'does not raise an error' do
  538. expect { article }.not_to raise_error
  539. end
  540. end
  541. shared_examples 'raising an error' do
  542. it 'raises an error' do
  543. expect { article }.to raise_error(Exceptions::InvalidAttribute, 'Sending an email without a valid recipient is not possible.')
  544. end
  545. end
  546. context 'when the validation is not explicitly turned on' do
  547. it_behaves_like 'not raising an error'
  548. end
  549. context 'when the validation is explicitly turned on' do
  550. let(:validate) { true }
  551. it_behaves_like 'raising an error'
  552. context 'when the system is in the import mode' do
  553. before do
  554. Setting.set('import_mode', true)
  555. end
  556. it_behaves_like 'not raising an error'
  557. end
  558. context 'when the type is not an email' do
  559. let(:type) { 'phone' }
  560. it_behaves_like 'not raising an error'
  561. end
  562. context 'when the recipient is empty' do
  563. let(:to) { '' }
  564. it_behaves_like 'raising an error'
  565. end
  566. context 'when the recipient is unparseable' do
  567. let(:to) { '@unparseable_address' }
  568. it_behaves_like 'raising an error'
  569. end
  570. context 'when the recipient is not a valid email address' do
  571. let(:to) { 'not_a_mail' }
  572. it_behaves_like 'raising an error'
  573. end
  574. end
  575. end
  576. end
  577. describe 'clone attachments' do
  578. context 'of forwarded article' do
  579. context 'via email' do
  580. it 'only need to clone attached attachments' do
  581. article_parent = create(:ticket_article,
  582. type: Ticket::Article::Type.find_by(name: 'email'),
  583. content_type: 'text/html',
  584. body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
  585. create(:store,
  586. object: 'Ticket::Article',
  587. o_id: article_parent.id,
  588. data: 'content_file1_normally_should_be_an_image',
  589. filename: 'some_file1.jpg',
  590. preferences: {
  591. 'Content-Type' => 'image/jpeg',
  592. 'Mime-Type' => 'image/jpeg',
  593. 'Content-ID' => '15.274327094.140938@zammad.example.com',
  594. 'Content-Disposition' => 'inline',
  595. })
  596. create(:store,
  597. object: 'Ticket::Article',
  598. o_id: article_parent.id,
  599. data: 'content_file2_normally_should_be_an_image',
  600. filename: 'some_file2.jpg',
  601. preferences: {
  602. 'Content-Type' => 'image/jpeg',
  603. 'Mime-Type' => 'image/jpeg',
  604. 'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
  605. 'Content-Disposition' => 'inline',
  606. })
  607. create(:store,
  608. object: 'Ticket::Article',
  609. o_id: article_parent.id,
  610. data: 'content_file3_normally_should_be_an_image',
  611. filename: 'some_file3.jpg',
  612. preferences: {
  613. 'Content-Type' => 'image/jpeg',
  614. 'Mime-Type' => 'image/jpeg',
  615. 'Content-Disposition' => 'attached',
  616. })
  617. article_new = create(:ticket_article)
  618. UserInfo.current_user_id = 1
  619. attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
  620. expect(attachments.count).to eq(2)
  621. expect(attachments[0].filename).to eq('some_file2.jpg')
  622. expect(attachments[1].filename).to eq('some_file3.jpg')
  623. end
  624. end
  625. end
  626. context 'of trigger' do
  627. context 'via email notifications' do
  628. it 'only need to clone inline attachments used in body' do
  629. article_parent = create(:ticket_article,
  630. type: Ticket::Article::Type.find_by(name: 'email'),
  631. content_type: 'text/html',
  632. body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
  633. create(:store,
  634. object: 'Ticket::Article',
  635. o_id: article_parent.id,
  636. data: 'content_file1_normally_should_be_an_image',
  637. filename: 'some_file1.jpg',
  638. preferences: {
  639. 'Content-Type' => 'image/jpeg',
  640. 'Mime-Type' => 'image/jpeg',
  641. 'Content-ID' => '15.274327094.140938@zammad.example.com',
  642. 'Content-Disposition' => 'inline',
  643. })
  644. create(:store,
  645. object: 'Ticket::Article',
  646. o_id: article_parent.id,
  647. data: 'content_file2_normally_should_be_an_image',
  648. filename: 'some_file2.jpg',
  649. preferences: {
  650. 'Content-Type' => 'image/jpeg',
  651. 'Mime-Type' => 'image/jpeg',
  652. 'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
  653. 'Content-Disposition' => 'inline',
  654. })
  655. # #2483 - #{article.body_as_html} now includes attachments (e.g. PDFs)
  656. # Regular attachments do not get assigned a Content-ID, and should not be copied in this use case
  657. create(:store,
  658. object: 'Ticket::Article',
  659. o_id: article_parent.id,
  660. data: 'content_file3_with_no_content_id',
  661. filename: 'some_file3.jpg',
  662. preferences: {
  663. 'Content-Type' => 'image/jpeg',
  664. 'Mime-Type' => 'image/jpeg',
  665. })
  666. article_new = create(:ticket_article)
  667. UserInfo.current_user_id = 1
  668. attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
  669. expect(attachments.count).to eq(1)
  670. expect(attachments[0].filename).to eq('some_file1.jpg')
  671. end
  672. end
  673. end
  674. end
  675. end