ticket_spec.rb 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. require 'rails_helper'
  2. require 'models/application_model_examples'
  3. require 'models/concerns/can_be_imported_examples'
  4. require 'models/concerns/can_csv_import_examples'
  5. require 'models/concerns/has_history_examples'
  6. require 'models/concerns/has_tags_examples'
  7. require 'models/concerns/has_xss_sanitized_note_examples'
  8. require 'models/concerns/has_object_manager_attributes_validation_examples'
  9. RSpec.describe Ticket, type: :model do
  10. it_behaves_like 'ApplicationModel'
  11. it_behaves_like 'CanBeImported'
  12. it_behaves_like 'CanCsvImport'
  13. it_behaves_like 'HasHistory', history_relation_object: 'Ticket::Article'
  14. it_behaves_like 'HasTags'
  15. it_behaves_like 'HasXssSanitizedNote', model_factory: :ticket
  16. it_behaves_like 'HasObjectManagerAttributesValidation'
  17. subject(:ticket) { create(:ticket) }
  18. describe 'Class methods:' do
  19. describe '.selectors' do
  20. # https://github.com/zammad/zammad/issues/1769
  21. context 'when matching multiple tickets, each with multiple articles' do
  22. let(:tickets) { create_list(:ticket, 2) }
  23. before do
  24. create(:ticket_article, ticket: tickets.first, from: 'asdf1@blubselector.de')
  25. create(:ticket_article, ticket: tickets.first, from: 'asdf2@blubselector.de')
  26. create(:ticket_article, ticket: tickets.first, from: 'asdf3@blubselector.de')
  27. create(:ticket_article, ticket: tickets.last, from: 'asdf4@blubselector.de')
  28. create(:ticket_article, ticket: tickets.last, from: 'asdf5@blubselector.de')
  29. create(:ticket_article, ticket: tickets.last, from: 'asdf6@blubselector.de')
  30. end
  31. let(:condition) do
  32. {
  33. 'article.from' => {
  34. operator: 'contains',
  35. value: 'blubselector.de',
  36. },
  37. }
  38. end
  39. it 'returns a list of unique tickets (i.e., no duplicates)' do
  40. expect(described_class.selectors(condition, limit: 100, access: 'full'))
  41. .to match_array([2, tickets.to_a])
  42. end
  43. end
  44. end
  45. end
  46. describe 'Instance methods:' do
  47. describe '#merge_to' do
  48. let(:target_ticket) { create(:ticket) }
  49. context 'when source ticket has Links' do
  50. let(:linked_tickets) { create_list(:ticket, 3) }
  51. let(:links) { linked_tickets.map { |l| create(:link, from: ticket, to: l) } }
  52. it 'reassigns all links to the target ticket after merge' do
  53. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  54. .to change { links.each(&:reload).map(&:link_object_source_value) }
  55. .to(Array.new(3) { target_ticket.id })
  56. end
  57. end
  58. context 'when attempting to cross-merge (i.e., to merge B → A after merging A → B)' do
  59. before { target_ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  60. it 'raises an error' do
  61. expect { ticket.merge_to(ticket_id: target_ticket.id, user_id: 1) }
  62. .to raise_error('ticket already merged, no merge into merged ticket possible')
  63. end
  64. end
  65. context 'when attempting to self-merge (i.e., to merge A → A)' do
  66. it 'raises an error' do
  67. expect { ticket.merge_to(ticket_id: ticket.id, user_id: 1) }
  68. .to raise_error("Can't merge ticket with it self!")
  69. end
  70. end
  71. # Issue #2469 - Add information "Ticket merged" to History
  72. context 'when merging' do
  73. let(:merge_user) { create(:user) }
  74. before do
  75. # create target ticket early
  76. # to avoid a race condition
  77. # when creating the history entries
  78. target_ticket
  79. travel 5.minutes
  80. end
  81. it 'creates history entries in both the origin ticket and the target ticket' do
  82. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  83. expect(target_ticket.history_get.size).to eq 2
  84. target_history = target_ticket.history_get.last
  85. expect(target_history['object']).to eq 'Ticket'
  86. expect(target_history['type']).to eq 'received_merge'
  87. expect(target_history['created_by_id']).to eq merge_user.id
  88. expect(target_history['o_id']).to eq target_ticket.id
  89. expect(target_history['id_to']).to eq target_ticket.id
  90. expect(target_history['id_from']).to eq ticket.id
  91. expect(ticket.history_get.size).to eq 4
  92. origin_history = ticket.reload.history_get[1]
  93. expect(origin_history['object']).to eq 'Ticket'
  94. expect(origin_history['type']).to eq 'merged_into'
  95. expect(origin_history['created_by_id']).to eq merge_user.id
  96. expect(origin_history['o_id']).to eq ticket.id
  97. expect(origin_history['id_to']).to eq target_ticket.id
  98. expect(origin_history['id_from']).to eq ticket.id
  99. end
  100. end
  101. end
  102. describe '#perform_changes' do
  103. # Regression test for https://github.com/zammad/zammad/issues/2001
  104. describe 'argument handling' do
  105. let(:perform) do
  106. {
  107. 'notification.email' => {
  108. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  109. recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
  110. subject: "Autoclose (\#{ticket.title})"
  111. }
  112. }
  113. end
  114. it 'does not mutate contents of "perform" hash' do
  115. expect { ticket.perform_changes(perform, 'trigger', {}, 1) }
  116. .not_to change { perform }
  117. end
  118. end
  119. context 'with "ticket.state_id" key in "perform" hash' do
  120. let(:perform) do
  121. {
  122. 'ticket.state_id' => {
  123. 'value' => Ticket::State.lookup(name: 'closed').id
  124. }
  125. }
  126. end
  127. it 'changes #state to specified value' do
  128. expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
  129. .to change { ticket.reload.state.name }.to('closed')
  130. end
  131. end
  132. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  133. let(:perform) do
  134. {
  135. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  136. 'ticket.action' => { 'value' => 'delete' },
  137. }
  138. end
  139. it 'performs a ticket deletion on a ticket' do
  140. expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
  141. .to change(ticket, :destroyed?).to(true)
  142. end
  143. end
  144. context 'with a "notification.email" trigger' do
  145. # Regression test for https://github.com/zammad/zammad/issues/1543
  146. #
  147. # If a new article fires an email notification trigger,
  148. # and then another article is added to the same ticket
  149. # before that trigger is performed,
  150. # the email template's 'article' var should refer to the originating article,
  151. # not the newest one.
  152. #
  153. # (This occurs whenever one action fires multiple email notification triggers.)
  154. context 'when two articles are created before the trigger fires once (race condition)' do
  155. let!(:article) { create(:ticket_article, ticket: ticket) }
  156. let!(:new_article) { create(:ticket_article, ticket: ticket) }
  157. let(:trigger) do
  158. build(:trigger,
  159. perform: {
  160. 'notification.email' => {
  161. body: '',
  162. recipient: 'ticket_customer',
  163. subject: ''
  164. }
  165. })
  166. end
  167. # required by Ticket#perform_changes for email notifications
  168. before { article.ticket.group.update(email_address: create(:email_address)) }
  169. it 'passes the first article to NotificationFactory::Mailer' do
  170. expect(NotificationFactory::Mailer)
  171. .to receive(:template)
  172. .with(hash_including(objects: { ticket: ticket, article: article }))
  173. .at_least(:once)
  174. .and_call_original
  175. expect(NotificationFactory::Mailer)
  176. .not_to receive(:template)
  177. .with(hash_including(objects: { ticket: ticket, article: new_article }))
  178. ticket.perform_changes(trigger.perform, 'trigger', { article_id: article.id }, 1)
  179. end
  180. end
  181. end
  182. end
  183. describe '#access?' do
  184. context 'when given ticket’s owner' do
  185. it 'returns true for both "read" and "full" privileges' do
  186. expect(ticket.access?(ticket.owner, 'read')).to be(true)
  187. expect(ticket.access?(ticket.owner, 'full')).to be(true)
  188. end
  189. end
  190. context 'when given the ticket’s customer' do
  191. it 'returns true for both "read" and "full" privileges' do
  192. expect(ticket.access?(ticket.customer, 'read')).to be(true)
  193. expect(ticket.access?(ticket.customer, 'full')).to be(true)
  194. end
  195. end
  196. context 'when given a user that is neither owner nor customer' do
  197. let(:user) { create(:agent_user) }
  198. it 'returns false for both "read" and "full" privileges' do
  199. expect(ticket.access?(user, 'read')).to be(false)
  200. expect(ticket.access?(user, 'full')).to be(false)
  201. end
  202. context 'but the user is an agent with full access to ticket’s group' do
  203. before { user.group_names_access_map = { ticket.group.name => 'full' } }
  204. it 'returns true for both "read" and "full" privileges' do
  205. expect(ticket.access?(user, 'read')).to be(true)
  206. expect(ticket.access?(user, 'full')).to be(true)
  207. end
  208. end
  209. context 'but the user is a customer from the same organization as ticket’s customer' do
  210. subject(:ticket) { create(:ticket, customer: customer) }
  211. let(:customer) { create(:customer_user, organization: create(:organization)) }
  212. let(:colleague) { create(:customer_user, organization: customer.organization) }
  213. context 'and organization.shared is true (default)' do
  214. it 'returns true for both "read" and "full" privileges' do
  215. expect(ticket.access?(colleague, 'read')).to be(true)
  216. expect(ticket.access?(colleague, 'full')).to be(true)
  217. end
  218. end
  219. context 'but organization.shared is false' do
  220. before { customer.organization.update(shared: false) }
  221. it 'returns false for both "read" and "full" privileges' do
  222. expect(ticket.access?(colleague, 'read')).to be(false)
  223. expect(ticket.access?(colleague, 'full')).to be(false)
  224. end
  225. end
  226. end
  227. end
  228. end
  229. describe '#subject_build' do
  230. context 'with default "ticket_hook_position" setting ("right")' do
  231. it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
  232. expect(ticket.subject_build('foo'))
  233. .to eq("foo [Ticket##{ticket.number}]")
  234. end
  235. context 'and a non-default value for the "ticket_hook" setting' do
  236. before { Setting.set('ticket_hook', 'bar baz') }
  237. it 'replaces "Ticket#" with the new ticket hook' do
  238. expect(ticket.subject_build('foo'))
  239. .to eq("foo [bar baz#{ticket.number}]")
  240. end
  241. end
  242. context 'and a non-default value for the "ticket_hook_divider" setting' do
  243. before { Setting.set('ticket_hook_divider', ': ') }
  244. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  245. expect(ticket.subject_build('foo'))
  246. .to eq("foo [Ticket#: #{ticket.number}]")
  247. end
  248. end
  249. context 'when the given string already contains a ticket reference, but in the wrong place' do
  250. it 'moves the ticket reference to the end' do
  251. expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
  252. .to eq("foo [Ticket##{ticket.number}]")
  253. end
  254. end
  255. context 'when the given string already contains an alternately formatted ticket reference' do
  256. it 'reformats the ticket reference' do
  257. expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
  258. .to eq("foo [Ticket##{ticket.number}]")
  259. end
  260. end
  261. end
  262. context 'with alternate "ticket_hook_position" setting ("left")' do
  263. before { Setting.set('ticket_hook_position', 'left') }
  264. it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
  265. expect(ticket.subject_build('foo'))
  266. .to eq("[Ticket##{ticket.number}] foo")
  267. end
  268. context 'and a non-default value for the "ticket_hook" setting' do
  269. before { Setting.set('ticket_hook', 'bar baz') }
  270. it 'replaces "Ticket#" with the new ticket hook' do
  271. expect(ticket.subject_build('foo'))
  272. .to eq("[bar baz#{ticket.number}] foo")
  273. end
  274. end
  275. context 'and a non-default value for the "ticket_hook_divider" setting' do
  276. before { Setting.set('ticket_hook_divider', ': ') }
  277. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  278. expect(ticket.subject_build('foo'))
  279. .to eq("[Ticket#: #{ticket.number}] foo")
  280. end
  281. end
  282. context 'when the given string already contains a ticket reference, but in the wrong place' do
  283. it 'moves the ticket reference to the start' do
  284. expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
  285. .to eq("[Ticket##{ticket.number}] foo")
  286. end
  287. end
  288. context 'when the given string already contains an alternately formatted ticket reference' do
  289. it 'reformats the ticket reference' do
  290. expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
  291. .to eq("[Ticket##{ticket.number}] foo")
  292. end
  293. end
  294. end
  295. end
  296. end
  297. describe 'Attributes:' do
  298. describe '#owner' do
  299. let(:original_owner) { create(:agent_user, groups: [ticket.group]) }
  300. before { ticket.update(owner: original_owner) }
  301. context 'when assigned directly' do
  302. context 'to an active agent belonging to ticket.group' do
  303. let(:agent) { create(:agent_user, groups: [ticket.group]) }
  304. it 'can be set' do
  305. expect { ticket.update(owner: agent) }
  306. .to change { ticket.reload.owner }.to(agent)
  307. end
  308. end
  309. context 'to an agent not belonging to ticket.group' do
  310. let(:agent) { create(:agent_user, groups: [other_group]) }
  311. let(:other_group) { create(:group) }
  312. it 'resets to default user (id: 1) instead' do
  313. expect { ticket.update(owner: agent) }
  314. .to change { ticket.reload.owner }.to(User.first)
  315. end
  316. end
  317. context 'to an inactive agent' do
  318. let(:agent) { create(:agent_user, groups: [ticket.group], active: false) }
  319. it 'resets to default user (id: 1) instead' do
  320. expect { ticket.update(owner: agent) }
  321. .to change { ticket.reload.owner }.to(User.first)
  322. end
  323. end
  324. context 'to a non-agent' do
  325. let(:agent) { create(:customer_user, groups: [ticket.group]) }
  326. it 'resets to default user (id: 1) instead' do
  327. expect { ticket.update(owner: agent) }
  328. .to change { ticket.reload.owner }.to(User.first)
  329. end
  330. end
  331. end
  332. context 'when the ticket is updated for any other reason' do
  333. context 'if original owner is still an active agent belonging to ticket.group' do
  334. it 'does not change' do
  335. expect { create(:ticket_article, ticket: ticket) }
  336. .not_to change { ticket.reload.owner }
  337. end
  338. end
  339. context 'if original owner has left ticket.group' do
  340. before { original_owner.groups = [] }
  341. it 'resets to default user (id: 1)' do
  342. expect { create(:ticket_article, ticket: ticket) }
  343. .to change { ticket.reload.owner }.to(User.first)
  344. end
  345. end
  346. context 'if original owner has become inactive' do
  347. before { original_owner.update(active: false) }
  348. it 'resets to default user (id: 1)' do
  349. expect { create(:ticket_article, ticket: ticket) }
  350. .to change { ticket.reload.owner }.to(User.first)
  351. end
  352. end
  353. context 'if original owner has lost agent status' do
  354. before { original_owner.roles = [create(:role)] }
  355. it 'resets to default user (id: 1)' do
  356. expect { create(:ticket_article, ticket: ticket) }
  357. .to change { ticket.reload.owner }.to(User.first)
  358. end
  359. end
  360. end
  361. end
  362. describe '#state' do
  363. context 'when originally "new" (default)' do
  364. context 'and a customer article is added' do
  365. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
  366. it 'stays "new"' do
  367. expect { article }
  368. .not_to change { ticket.state.name }.from('new')
  369. end
  370. end
  371. context 'and a non-customer article is added' do
  372. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  373. it 'switches to "open"' do
  374. expect { article }
  375. .to change { ticket.reload.state.name }.from('new').to('open')
  376. end
  377. end
  378. end
  379. context 'when originally "closed"' do
  380. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  381. context 'when a non-customer article is added' do
  382. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  383. it 'stays "closed"' do
  384. expect { article }.not_to change { ticket.reload.state.name }
  385. end
  386. end
  387. end
  388. end
  389. describe '#pending_time' do
  390. subject(:ticket) { create(:ticket, pending_time: Time.zone.now + 2.days) }
  391. context 'when #state is updated to any non-"pending" value' do
  392. it 'is reset to nil' do
  393. expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
  394. .to change(ticket, :pending_time).to(nil)
  395. end
  396. end
  397. # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
  398. context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
  399. it 'does not prematurely raise within the callback (#reset_pending_time)' do
  400. expect { ticket.update!(state: nil) }
  401. .to raise_error(ActiveRecord::StatementInvalid)
  402. end
  403. end
  404. end
  405. describe '#escalation_at' do
  406. before { travel_to(Time.current) } # freeze time
  407. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  408. let(:calendar) { create(:calendar, :'24/7') }
  409. context 'with no SLAs in the system' do
  410. it 'defaults to nil' do
  411. expect(ticket.escalation_at).to be(nil)
  412. end
  413. end
  414. context 'with an SLA in the system' do
  415. before { sla } # create sla
  416. it 'is set based on SLA’s #first_response_time' do
  417. expect(ticket.reload.escalation_at.to_i)
  418. .to eq(1.hour.from_now.to_i)
  419. end
  420. context 'after first agent’s response' do
  421. before { ticket } # create ticket
  422. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  423. it 'is updated based on the SLA’s #update_time' do
  424. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  425. expect { article }
  426. .to change { ticket.reload.escalation_at.to_i }
  427. .to eq(3.hours.from_now.to_i)
  428. end
  429. context 'when new #update_time is later than original #solution_time' do
  430. it 'is updated based on the original #solution_time' do
  431. travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  432. expect { article }
  433. .to change { ticket.reload.escalation_at.to_i }
  434. .to eq(4.hours.after(ticket.created_at).to_i)
  435. end
  436. end
  437. end
  438. end
  439. context 'when updated after an SLA has been added to the system' do
  440. before do
  441. ticket # create ticket
  442. sla # create sla
  443. end
  444. it 'is updated based on the new SLA’s #first_response_time' do
  445. expect { ticket.save! }
  446. .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
  447. end
  448. end
  449. context 'when updated after all SLAs have been removed from the system' do
  450. before do
  451. sla # create sla
  452. ticket # create ticket
  453. sla.destroy
  454. end
  455. it 'is set to nil' do
  456. expect { ticket.save! }
  457. .to change { ticket.reload.escalation_at }.to(nil)
  458. end
  459. end
  460. end
  461. describe '#first_response_escalation_at' do
  462. before { travel_to(Time.current) } # freeze time
  463. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  464. let(:calendar) { create(:calendar, :'24/7') }
  465. context 'with no SLAs in the system' do
  466. it 'defaults to nil' do
  467. expect(ticket.first_response_escalation_at).to be(nil)
  468. end
  469. end
  470. context 'with an SLA in the system' do
  471. before { sla } # create sla
  472. it 'is set based on SLA’s #first_response_time' do
  473. expect(ticket.reload.first_response_escalation_at.to_i)
  474. .to eq(1.hour.from_now.to_i)
  475. end
  476. context 'after first agent’s response' do
  477. before { ticket } # create ticket
  478. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  479. it 'does not change' do
  480. expect { article }.not_to change(ticket, :first_response_escalation_at)
  481. end
  482. end
  483. end
  484. end
  485. describe '#update_escalation_at' do
  486. before { travel_to(Time.current) } # freeze time
  487. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  488. let(:calendar) { create(:calendar, :'24/7') }
  489. context 'with no SLAs in the system' do
  490. it 'defaults to nil' do
  491. expect(ticket.update_escalation_at).to be(nil)
  492. end
  493. end
  494. context 'with an SLA in the system' do
  495. before { sla } # create sla
  496. it 'is set based on SLA’s #update_time' do
  497. expect(ticket.reload.update_escalation_at.to_i)
  498. .to eq(3.hours.from_now.to_i)
  499. end
  500. context 'after first agent’s response' do
  501. before { ticket } # create ticket
  502. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  503. it 'is updated based on the SLA’s #update_time' do
  504. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  505. expect { article }
  506. .to change { ticket.reload.update_escalation_at.to_i }
  507. .to(3.hours.from_now.to_i)
  508. end
  509. end
  510. end
  511. end
  512. describe '#close_escalation_at' do
  513. before { travel_to(Time.current) } # freeze time
  514. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  515. let(:calendar) { create(:calendar, :'24/7') }
  516. context 'with no SLAs in the system' do
  517. it 'defaults to nil' do
  518. expect(ticket.close_escalation_at).to be(nil)
  519. end
  520. end
  521. context 'with an SLA in the system' do
  522. before { sla } # create sla
  523. it 'is set based on SLA’s #solution_time' do
  524. expect(ticket.reload.close_escalation_at.to_i)
  525. .to eq(4.hours.from_now.to_i)
  526. end
  527. context 'after first agent’s response' do
  528. before { ticket } # create ticket
  529. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  530. it 'does not change' do
  531. expect { article }.not_to change(ticket, :close_escalation_at)
  532. end
  533. end
  534. end
  535. end
  536. end
  537. describe 'Associations:' do
  538. describe '#organization' do
  539. subject(:ticket) { build(:ticket, customer: customer, organization: nil) }
  540. let(:customer) { create(:customer, :with_org) }
  541. context 'on creation' do
  542. it 'automatically adopts the organization of its #customer' do
  543. expect { ticket.save }
  544. .to change(ticket, :organization).to(customer.organization)
  545. end
  546. end
  547. context 'on update of #customer.organization' do
  548. context 'to nil' do
  549. it 'automatically updates to #customer’s new value' do
  550. ticket.save
  551. expect { customer.update(organization: nil) }
  552. .to change { ticket.reload.organization }.to(nil)
  553. end
  554. end
  555. context 'to a different organization' do
  556. let(:new_org) { create(:organization) }
  557. it 'automatically updates to #customer’s new value' do
  558. ticket.save
  559. expect { customer.update(organization: new_org) }
  560. .to change { ticket.reload.organization }.to(new_org)
  561. end
  562. end
  563. end
  564. end
  565. end
  566. describe 'Callbacks & Observers -' do
  567. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  568. it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
  569. expect { create(:ticket, title: "some title \u0000 123") }
  570. .not_to raise_error
  571. end
  572. end
  573. describe 'XSS protection:' do
  574. subject(:ticket) { create(:ticket, title: title) }
  575. let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
  576. it 'does not sanitize title' do
  577. expect(ticket.title).to eq(title)
  578. end
  579. end
  580. describe 'Cti::CallerId syncing:' do
  581. subject(:ticket) { build(:ticket) }
  582. before { allow(Cti::CallerId).to receive(:build) }
  583. it 'adds numbers in article bodies (via Cti::CallerId.build)' do
  584. expect(Cti::CallerId).to receive(:build).with(ticket)
  585. ticket.save
  586. Observer::Transaction.commit
  587. Scheduler.worker(true)
  588. end
  589. end
  590. describe 'Touching associations on update:' do
  591. subject(:ticket) { create(:ticket, customer: customer) }
  592. let(:customer) { create(:customer_user, organization: organization) }
  593. let(:organization) { create(:organization) }
  594. let(:other_customer) { create(:customer_user, organization: other_organization) }
  595. let(:other_organization) { create(:organization) }
  596. context 'on creation' do
  597. it 'touches its customer and his organization' do
  598. expect { ticket }
  599. .to change { customer.reload.updated_at }
  600. .and change { organization.reload.updated_at }
  601. end
  602. end
  603. context 'on destruction' do
  604. before { ticket }
  605. it 'touches its customer and his organization' do
  606. expect { ticket.destroy }
  607. .to change { customer.reload.updated_at }
  608. .and change { organization.reload.updated_at }
  609. end
  610. end
  611. context 'when customer association is changed' do
  612. it 'touches both old and new customer, and their organizations' do
  613. expect { ticket.update(customer: other_customer) }
  614. .to change { customer.reload.updated_at }
  615. .and change { organization.reload.updated_at }
  616. .and change { other_customer.reload.updated_at }
  617. .and change { other_organization.reload.updated_at }
  618. end
  619. end
  620. context 'when organization has 100+ members' do
  621. let!(:other_members) { create_list(:user, 100, organization: organization) }
  622. context 'and customer association is changed' do
  623. it 'touches both old and new customer, and their organizations' do
  624. expect { ticket.update(customer: other_customer) }
  625. .to change { customer.reload.updated_at }
  626. .and change { organization.reload.updated_at }
  627. .and change { other_customer.reload.updated_at }
  628. .and change { other_organization.reload.updated_at }
  629. end
  630. end
  631. end
  632. end
  633. describe 'Association & attachment management:' do
  634. it 'deletes all related ActivityStreams on destroy' do
  635. create_list(:activity_stream, 3, o: ticket)
  636. expect { ticket.destroy }
  637. .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  638. .to(false)
  639. end
  640. it 'deletes all related Links on destroy' do
  641. create(:link, from: ticket, to: create(:ticket))
  642. create(:link, from: create(:ticket), to: ticket)
  643. create(:link, from: ticket, to: create(:ticket))
  644. expect { ticket.destroy }
  645. .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
  646. .to(false)
  647. end
  648. it 'deletes all related Articles on destroy' do
  649. create_list(:ticket_article, 3, ticket: ticket)
  650. expect { ticket.destroy }
  651. .to change { Ticket::Article.exists?(ticket: ticket) }
  652. .to(false)
  653. end
  654. it 'deletes all related OnlineNotifications on destroy' do
  655. create_list(:online_notification, 3, o: ticket)
  656. expect { ticket.destroy }
  657. .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
  658. .to(false)
  659. end
  660. it 'deletes all related Tags on destroy' do
  661. create_list(:tag, 3, o: ticket)
  662. expect { ticket.destroy }
  663. .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  664. .to(false)
  665. end
  666. it 'deletes all related Histories on destroy' do
  667. create_list(:history, 3, o: ticket)
  668. expect { ticket.destroy }
  669. .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  670. .to(false)
  671. end
  672. it 'deletes all related Karma::ActivityLogs on destroy' do
  673. create_list(:'karma/activity_log', 3, o: ticket)
  674. expect { ticket.destroy }
  675. .to change { Karma::ActivityLog.exists?(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  676. .to(false)
  677. end
  678. it 'deletes all related RecentViews on destroy' do
  679. create_list(:recent_view, 3, o: ticket)
  680. expect { ticket.destroy }
  681. .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  682. .to(false)
  683. end
  684. context 'when ticket is generated from email (with attachments)' do
  685. subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
  686. let(:raw_email) { File.read(Rails.root.join('test', 'data', 'mail', 'mail001.box')) }
  687. it 'adds attachments to the Store{::File,::Provider::DB} tables' do
  688. expect { ticket }
  689. .to change(Store, :count).by(2)
  690. .and change { Store::File.count }.by(2)
  691. .and change { Store::Provider::DB.count }.by(2)
  692. end
  693. context 'and subsequently destroyed' do
  694. it 'deletes all related attachments' do
  695. ticket # create ticket
  696. expect { ticket.destroy }
  697. .to change(Store, :count).by(-2)
  698. .and change { Store::File.count }.by(-2)
  699. .and change { Store::Provider::DB.count }.by(-2)
  700. end
  701. end
  702. context 'and a duplicate ticket is generated from the same email' do
  703. before { ticket } # create ticket
  704. let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
  705. it 'adds duplicate attachments to the Store table only' do
  706. expect { duplicate }
  707. .to change(Store, :count).by(2)
  708. .and change { Store::File.count }.by(0)
  709. .and change { Store::Provider::DB.count }.by(0)
  710. end
  711. context 'when only the duplicate ticket is destroyed' do
  712. it 'deletes only the duplicate attachments' do
  713. duplicate # create ticket
  714. expect { duplicate.destroy }
  715. .to change(Store, :count).by(-2)
  716. .and change { Store::File.count }.by(0)
  717. .and change { Store::Provider::DB.count }.by(0)
  718. end
  719. end
  720. context 'when only the duplicate ticket is destroyed' do
  721. it 'deletes all related attachments' do
  722. duplicate.destroy
  723. expect { ticket.destroy }
  724. .to change(Store, :count).by(-2)
  725. .and change { Store::File.count }.by(-2)
  726. .and change { Store::Provider::DB.count }.by(-2)
  727. end
  728. end
  729. end
  730. end
  731. end
  732. end
  733. end