ticket_spec.rb 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974
  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. it 'sends ExternalSync.migrate' do
  101. allow(ExternalSync).to receive(:migrate)
  102. ticket.merge_to(ticket_id: target_ticket.id, user_id: merge_user.id)
  103. expect(ExternalSync).to have_received(:migrate).with('Ticket', ticket.id, target_ticket.id)
  104. end
  105. end
  106. end
  107. describe '#perform_changes' do
  108. # Regression test for https://github.com/zammad/zammad/issues/2001
  109. describe 'argument handling' do
  110. let(:perform) do
  111. {
  112. 'notification.email' => {
  113. body: "Hello \#{ticket.customer.firstname} \#{ticket.customer.lastname},",
  114. recipient: %w[article_last_sender ticket_owner ticket_customer ticket_agents],
  115. subject: "Autoclose (\#{ticket.title})"
  116. }
  117. }
  118. end
  119. it 'does not mutate contents of "perform" hash' do
  120. expect { ticket.perform_changes(perform, 'trigger', {}, 1) }
  121. .not_to change { perform }
  122. end
  123. end
  124. context 'with "ticket.state_id" key in "perform" hash' do
  125. let(:perform) do
  126. {
  127. 'ticket.state_id' => {
  128. 'value' => Ticket::State.lookup(name: 'closed').id
  129. }
  130. }
  131. end
  132. it 'changes #state to specified value' do
  133. expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
  134. .to change { ticket.reload.state.name }.to('closed')
  135. end
  136. end
  137. context 'with "ticket.action" => { "value" => "delete" } in "perform" hash' do
  138. let(:perform) do
  139. {
  140. 'ticket.state_id' => { 'value' => Ticket::State.lookup(name: 'closed').id.to_s },
  141. 'ticket.action' => { 'value' => 'delete' },
  142. }
  143. end
  144. it 'performs a ticket deletion on a ticket' do
  145. expect { ticket.perform_changes(perform, 'trigger', ticket, User.first) }
  146. .to change(ticket, :destroyed?).to(true)
  147. end
  148. end
  149. context 'with a "notification.email" trigger' do
  150. # Regression test for https://github.com/zammad/zammad/issues/1543
  151. #
  152. # If a new article fires an email notification trigger,
  153. # and then another article is added to the same ticket
  154. # before that trigger is performed,
  155. # the email template's 'article' var should refer to the originating article,
  156. # not the newest one.
  157. #
  158. # (This occurs whenever one action fires multiple email notification triggers.)
  159. context 'when two articles are created before the trigger fires once (race condition)' do
  160. let!(:article) { create(:ticket_article, ticket: ticket) }
  161. let!(:new_article) { create(:ticket_article, ticket: ticket) }
  162. let(:trigger) do
  163. build(:trigger,
  164. perform: {
  165. 'notification.email' => {
  166. body: '',
  167. recipient: 'ticket_customer',
  168. subject: ''
  169. }
  170. })
  171. end
  172. # required by Ticket#perform_changes for email notifications
  173. before { article.ticket.group.update(email_address: create(:email_address)) }
  174. it 'passes the first article to NotificationFactory::Mailer' do
  175. expect(NotificationFactory::Mailer)
  176. .to receive(:template)
  177. .with(hash_including(objects: { ticket: ticket, article: article }))
  178. .at_least(:once)
  179. .and_call_original
  180. expect(NotificationFactory::Mailer)
  181. .not_to receive(:template)
  182. .with(hash_including(objects: { ticket: ticket, article: new_article }))
  183. ticket.perform_changes(trigger.perform, 'trigger', { article_id: article.id }, 1)
  184. end
  185. end
  186. end
  187. context 'with a notification trigger' do
  188. # https://github.com/zammad/zammad/issues/2782
  189. #
  190. # Notification triggers should log notification as private or public
  191. # according to given configuration
  192. let(:user) { create(:admin_user, mobile: '+37061010000') }
  193. before { ticket.group.users << user }
  194. let(:perform) do
  195. {
  196. notification_key => {
  197. body: 'Old programmers never die. They just branch to a new address.',
  198. recipient: 'ticket_agents',
  199. subject: 'Old programmers never die. They just branch to a new address.'
  200. }
  201. }.deep_merge(additional_options).deep_stringify_keys
  202. end
  203. let(:notification_key) { "notification.#{notification_type}" }
  204. shared_examples 'verify log visibility status' do
  205. shared_examples 'notification trigger' do
  206. it 'adds Ticket::Article' do
  207. expect { ticket.perform_changes(perform, 'trigger', ticket, user) }
  208. .to change { ticket.articles.count }.by(1)
  209. end
  210. it 'new Ticket::Article visibility reflects setting' do
  211. ticket.perform_changes(perform, 'trigger', ticket, User.first)
  212. new_article = ticket.articles.reload.last
  213. expect(new_article.internal).to be target_internal_value
  214. end
  215. end
  216. context 'when set to private' do
  217. let(:additional_options) do
  218. {
  219. notification_key => {
  220. internal: true
  221. }
  222. }
  223. end
  224. let(:target_internal_value) { true }
  225. it_behaves_like 'notification trigger'
  226. end
  227. context 'when set to internal' do
  228. let(:additional_options) do
  229. {
  230. notification_key => {
  231. internal: false
  232. }
  233. }
  234. end
  235. let(:target_internal_value) { false }
  236. it_behaves_like 'notification trigger'
  237. end
  238. context 'when no selection was made' do # ensure previously created triggers default to public
  239. let(:additional_options) do
  240. {}
  241. end
  242. let(:target_internal_value) { false }
  243. it_behaves_like 'notification trigger'
  244. end
  245. end
  246. context 'dispatching email' do
  247. let(:notification_type) { :email }
  248. include_examples 'verify log visibility status'
  249. end
  250. context 'dispatching SMS' do
  251. let(:notification_type) { :sms }
  252. before { create(:channel, area: 'Sms::Notification') }
  253. include_examples 'verify log visibility status'
  254. end
  255. end
  256. end
  257. describe '#subject_build' do
  258. context 'with default "ticket_hook_position" setting ("right")' do
  259. it 'returns the given string followed by a ticket reference (of the form "[Ticket#123]")' do
  260. expect(ticket.subject_build('foo'))
  261. .to eq("foo [Ticket##{ticket.number}]")
  262. end
  263. context 'and a non-default value for the "ticket_hook" setting' do
  264. before { Setting.set('ticket_hook', 'bar baz') }
  265. it 'replaces "Ticket#" with the new ticket hook' do
  266. expect(ticket.subject_build('foo'))
  267. .to eq("foo [bar baz#{ticket.number}]")
  268. end
  269. end
  270. context 'and a non-default value for the "ticket_hook_divider" setting' do
  271. before { Setting.set('ticket_hook_divider', ': ') }
  272. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  273. expect(ticket.subject_build('foo'))
  274. .to eq("foo [Ticket#: #{ticket.number}]")
  275. end
  276. end
  277. context 'when the given string already contains a ticket reference, but in the wrong place' do
  278. it 'moves the ticket reference to the end' do
  279. expect(ticket.subject_build("[Ticket##{ticket.number}] foo"))
  280. .to eq("foo [Ticket##{ticket.number}]")
  281. end
  282. end
  283. context 'when the given string already contains an alternately formatted ticket reference' do
  284. it 'reformats the ticket reference' do
  285. expect(ticket.subject_build("foo [Ticket#: #{ticket.number}]"))
  286. .to eq("foo [Ticket##{ticket.number}]")
  287. end
  288. end
  289. end
  290. context 'with alternate "ticket_hook_position" setting ("left")' do
  291. before { Setting.set('ticket_hook_position', 'left') }
  292. it 'returns a ticket reference (of the form "[Ticket#123]") followed by the given string' do
  293. expect(ticket.subject_build('foo'))
  294. .to eq("[Ticket##{ticket.number}] foo")
  295. end
  296. context 'and a non-default value for the "ticket_hook" setting' do
  297. before { Setting.set('ticket_hook', 'bar baz') }
  298. it 'replaces "Ticket#" with the new ticket hook' do
  299. expect(ticket.subject_build('foo'))
  300. .to eq("[bar baz#{ticket.number}] foo")
  301. end
  302. end
  303. context 'and a non-default value for the "ticket_hook_divider" setting' do
  304. before { Setting.set('ticket_hook_divider', ': ') }
  305. it 'inserts the new ticket hook divider between "Ticket#" and the ticket number' do
  306. expect(ticket.subject_build('foo'))
  307. .to eq("[Ticket#: #{ticket.number}] foo")
  308. end
  309. end
  310. context 'when the given string already contains a ticket reference, but in the wrong place' do
  311. it 'moves the ticket reference to the start' do
  312. expect(ticket.subject_build("foo [Ticket##{ticket.number}]"))
  313. .to eq("[Ticket##{ticket.number}] foo")
  314. end
  315. end
  316. context 'when the given string already contains an alternately formatted ticket reference' do
  317. it 'reformats the ticket reference' do
  318. expect(ticket.subject_build("[Ticket#: #{ticket.number}] foo"))
  319. .to eq("[Ticket##{ticket.number}] foo")
  320. end
  321. end
  322. end
  323. end
  324. end
  325. describe 'Attributes:' do
  326. describe '#owner' do
  327. let(:original_owner) { create(:agent_user, groups: [ticket.group]) }
  328. before { ticket.update(owner: original_owner) }
  329. context 'when assigned directly' do
  330. context 'to an active agent belonging to ticket.group' do
  331. let(:agent) { create(:agent_user, groups: [ticket.group]) }
  332. it 'can be set' do
  333. expect { ticket.update(owner: agent) }
  334. .to change { ticket.reload.owner }.to(agent)
  335. end
  336. end
  337. context 'to an agent not belonging to ticket.group' do
  338. let(:agent) { create(:agent_user, groups: [other_group]) }
  339. let(:other_group) { create(:group) }
  340. it 'resets to default user (id: 1) instead' do
  341. expect { ticket.update(owner: agent) }
  342. .to change { ticket.reload.owner }.to(User.first)
  343. end
  344. end
  345. context 'to an inactive agent' do
  346. let(:agent) { create(:agent_user, groups: [ticket.group], active: false) }
  347. it 'resets to default user (id: 1) instead' do
  348. expect { ticket.update(owner: agent) }
  349. .to change { ticket.reload.owner }.to(User.first)
  350. end
  351. end
  352. context 'to a non-agent' do
  353. let(:agent) { create(:customer_user, groups: [ticket.group]) }
  354. it 'resets to default user (id: 1) instead' do
  355. expect { ticket.update(owner: agent) }
  356. .to change { ticket.reload.owner }.to(User.first)
  357. end
  358. end
  359. end
  360. context 'when the ticket is updated for any other reason' do
  361. context 'if original owner is still an active agent belonging to ticket.group' do
  362. it 'does not change' do
  363. expect { create(:ticket_article, ticket: ticket) }
  364. .not_to change { ticket.reload.owner }
  365. end
  366. end
  367. context 'if original owner has left ticket.group' do
  368. before { original_owner.groups = [] }
  369. it 'resets to default user (id: 1)' do
  370. expect { create(:ticket_article, ticket: ticket) }
  371. .to change { ticket.reload.owner }.to(User.first)
  372. end
  373. end
  374. context 'if original owner has become inactive' do
  375. before { original_owner.update(active: false) }
  376. it 'resets to default user (id: 1)' do
  377. expect { create(:ticket_article, ticket: ticket) }
  378. .to change { ticket.reload.owner }.to(User.first)
  379. end
  380. end
  381. context 'if original owner has lost agent status' do
  382. before { original_owner.roles = [create(:role)] }
  383. it 'resets to default user (id: 1)' do
  384. expect { create(:ticket_article, ticket: ticket) }
  385. .to change { ticket.reload.owner }.to(User.first)
  386. end
  387. end
  388. context 'when the Ticket is closed' do
  389. before do
  390. ticket.update!(state: Ticket::State.lookup(name: 'closed'))
  391. end
  392. context 'if original owner is still an active agent belonging to ticket.group' do
  393. it 'does not change' do
  394. expect { create(:ticket_article, ticket: ticket) }
  395. .not_to change { ticket.reload.owner }
  396. end
  397. end
  398. context 'if original owner has left ticket.group' do
  399. before { original_owner.groups = [] }
  400. it 'does not change' do
  401. expect { create(:ticket_article, ticket: ticket) }
  402. .not_to change { ticket.reload.owner }
  403. end
  404. end
  405. context 'if original owner has become inactive' do
  406. before { original_owner.update(active: false) }
  407. it 'does not change' do
  408. expect { create(:ticket_article, ticket: ticket) }
  409. .not_to change { ticket.reload.owner }
  410. end
  411. end
  412. context 'if original owner has lost agent status' do
  413. before { original_owner.roles = [create(:role)] }
  414. it 'does not change' do
  415. expect { create(:ticket_article, ticket: ticket) }
  416. .not_to change { ticket.reload.owner }
  417. end
  418. end
  419. end
  420. end
  421. end
  422. describe '#state' do
  423. context 'when originally "new" (default)' do
  424. context 'and a customer article is added' do
  425. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Customer') }
  426. it 'stays "new"' do
  427. expect { article }
  428. .not_to change { ticket.state.name }.from('new')
  429. end
  430. end
  431. context 'and a non-customer article is added' do
  432. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  433. it 'switches to "open"' do
  434. expect { article }
  435. .to change { ticket.reload.state.name }.from('new').to('open')
  436. end
  437. end
  438. end
  439. context 'when originally "closed"' do
  440. before { ticket.update(state: Ticket::State.find_by(name: 'closed')) }
  441. context 'when a non-customer article is added' do
  442. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  443. it 'stays "closed"' do
  444. expect { article }.not_to change { ticket.reload.state.name }
  445. end
  446. end
  447. end
  448. end
  449. describe '#pending_time' do
  450. subject(:ticket) { create(:ticket, pending_time: Time.zone.now + 2.days) }
  451. context 'when #state is updated to any non-"pending" value' do
  452. it 'is reset to nil' do
  453. expect { ticket.update!(state: Ticket::State.lookup(name: 'open')) }
  454. .to change(ticket, :pending_time).to(nil)
  455. end
  456. end
  457. # Regression test for commit 92f227786f298bad1ccaf92d4478a7062ea6a49f
  458. context 'when #state is updated to nil (violating DB NOT NULL constraint)' do
  459. it 'does not prematurely raise within the callback (#reset_pending_time)' do
  460. expect { ticket.update!(state: nil) }
  461. .to raise_error(ActiveRecord::StatementInvalid)
  462. end
  463. end
  464. end
  465. describe '#escalation_at' do
  466. before { travel_to(Time.current) } # freeze time
  467. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  468. let(:calendar) { create(:calendar, :'24/7') }
  469. context 'with no SLAs in the system' do
  470. it 'defaults to nil' do
  471. expect(ticket.escalation_at).to be(nil)
  472. end
  473. end
  474. context 'with an SLA in the system' do
  475. before { sla } # create sla
  476. it 'is set based on SLA’s #first_response_time' do
  477. expect(ticket.reload.escalation_at.to_i)
  478. .to eq(1.hour.from_now.to_i)
  479. end
  480. context 'after first agent’s response' do
  481. before { ticket } # create ticket
  482. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  483. it 'is updated based on the SLA’s #update_time' do
  484. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  485. expect { article }
  486. .to change { ticket.reload.escalation_at.to_i }
  487. .to eq(3.hours.from_now.to_i)
  488. end
  489. context 'when new #update_time is later than original #solution_time' do
  490. it 'is updated based on the original #solution_time' do
  491. travel(2.hours) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  492. expect { article }
  493. .to change { ticket.reload.escalation_at.to_i }
  494. .to eq(4.hours.after(ticket.created_at).to_i)
  495. end
  496. end
  497. end
  498. end
  499. context 'when updated after an SLA has been added to the system' do
  500. before do
  501. ticket # create ticket
  502. sla # create sla
  503. end
  504. it 'is updated based on the new SLA’s #first_response_time' do
  505. expect { ticket.save! }
  506. .to change { ticket.reload.escalation_at.to_i }.from(0).to(1.hour.from_now.to_i)
  507. end
  508. end
  509. context 'when updated after all SLAs have been removed from the system' do
  510. before do
  511. sla # create sla
  512. ticket # create ticket
  513. sla.destroy
  514. end
  515. it 'is set to nil' do
  516. expect { ticket.save! }
  517. .to change { ticket.reload.escalation_at }.to(nil)
  518. end
  519. end
  520. end
  521. describe '#first_response_escalation_at' do
  522. before { travel_to(Time.current) } # freeze time
  523. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  524. let(:calendar) { create(:calendar, :'24/7') }
  525. context 'with no SLAs in the system' do
  526. it 'defaults to nil' do
  527. expect(ticket.first_response_escalation_at).to be(nil)
  528. end
  529. end
  530. context 'with an SLA in the system' do
  531. before { sla } # create sla
  532. it 'is set based on SLA’s #first_response_time' do
  533. expect(ticket.reload.first_response_escalation_at.to_i)
  534. .to eq(1.hour.from_now.to_i)
  535. end
  536. context 'after first agent’s response' do
  537. before { ticket } # create ticket
  538. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  539. it 'does not change' do
  540. expect { article }.not_to change(ticket, :first_response_escalation_at)
  541. end
  542. end
  543. end
  544. end
  545. describe '#update_escalation_at' do
  546. before { travel_to(Time.current) } # freeze time
  547. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  548. let(:calendar) { create(:calendar, :'24/7') }
  549. context 'with no SLAs in the system' do
  550. it 'defaults to nil' do
  551. expect(ticket.update_escalation_at).to be(nil)
  552. end
  553. end
  554. context 'with an SLA in the system' do
  555. before { sla } # create sla
  556. it 'is set based on SLA’s #update_time' do
  557. expect(ticket.reload.update_escalation_at.to_i)
  558. .to eq(3.hours.from_now.to_i)
  559. end
  560. context 'after first agent’s response' do
  561. before { ticket } # create ticket
  562. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  563. it 'is updated based on the SLA’s #update_time' do
  564. travel(1.minute) # time is frozen: if we don't travel forward, pre- and post-update values will be the same
  565. expect { article }
  566. .to change { ticket.reload.update_escalation_at.to_i }
  567. .to(3.hours.from_now.to_i)
  568. end
  569. end
  570. end
  571. end
  572. describe '#close_escalation_at' do
  573. before { travel_to(Time.current) } # freeze time
  574. let(:sla) { create(:sla, calendar: calendar, first_response_time: 60, update_time: 180, solution_time: 240) }
  575. let(:calendar) { create(:calendar, :'24/7') }
  576. context 'with no SLAs in the system' do
  577. it 'defaults to nil' do
  578. expect(ticket.close_escalation_at).to be(nil)
  579. end
  580. end
  581. context 'with an SLA in the system' do
  582. before { sla } # create sla
  583. it 'is set based on SLA’s #solution_time' do
  584. expect(ticket.reload.close_escalation_at.to_i)
  585. .to eq(4.hours.from_now.to_i)
  586. end
  587. context 'after first agent’s response' do
  588. before { ticket } # create ticket
  589. let(:article) { create(:ticket_article, ticket: ticket, sender_name: 'Agent') }
  590. it 'does not change' do
  591. expect { article }.not_to change(ticket, :close_escalation_at)
  592. end
  593. end
  594. end
  595. end
  596. end
  597. describe 'Associations:' do
  598. describe '#organization' do
  599. subject(:ticket) { build(:ticket, customer: customer, organization: nil) }
  600. let(:customer) { create(:customer, :with_org) }
  601. context 'on creation' do
  602. it 'automatically adopts the organization of its #customer' do
  603. expect { ticket.save }
  604. .to change(ticket, :organization).to(customer.organization)
  605. end
  606. end
  607. context 'on update of #customer.organization' do
  608. context 'to nil' do
  609. it 'automatically updates to #customer’s new value' do
  610. ticket.save
  611. expect { customer.update(organization: nil) }
  612. .to change { ticket.reload.organization }.to(nil)
  613. end
  614. end
  615. context 'to a different organization' do
  616. let(:new_org) { create(:organization) }
  617. it 'automatically updates to #customer’s new value' do
  618. ticket.save
  619. expect { customer.update(organization: new_org) }
  620. .to change { ticket.reload.organization }.to(new_org)
  621. end
  622. end
  623. end
  624. end
  625. end
  626. describe 'Callbacks & Observers -' do
  627. describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
  628. it 'removes them from title on creation, if necessary (postgres doesn’t like them)' do
  629. expect { create(:ticket, title: "some title \u0000 123") }
  630. .not_to raise_error
  631. end
  632. end
  633. describe 'XSS protection:' do
  634. subject(:ticket) { create(:ticket, title: title) }
  635. let(:title) { 'test 123 <script type="text/javascript">alert("XSS!");</script>' }
  636. it 'does not sanitize title' do
  637. expect(ticket.title).to eq(title)
  638. end
  639. end
  640. describe 'Cti::CallerId syncing:' do
  641. subject(:ticket) { build(:ticket) }
  642. before { allow(Cti::CallerId).to receive(:build) }
  643. it 'adds numbers in article bodies (via Cti::CallerId.build)' do
  644. expect(Cti::CallerId).to receive(:build).with(ticket)
  645. ticket.save
  646. Observer::Transaction.commit
  647. Scheduler.worker(true)
  648. end
  649. end
  650. describe 'Touching associations on update:' do
  651. subject(:ticket) { create(:ticket, customer: customer) }
  652. let(:customer) { create(:customer_user, organization: organization) }
  653. let(:organization) { create(:organization) }
  654. let(:other_customer) { create(:customer_user, organization: other_organization) }
  655. let(:other_organization) { create(:organization) }
  656. context 'on creation' do
  657. it 'touches its customer and his organization' do
  658. expect { ticket }
  659. .to change { customer.reload.updated_at }
  660. .and change { organization.reload.updated_at }
  661. end
  662. end
  663. context 'on destruction' do
  664. before { ticket }
  665. it 'touches its customer and his organization' do
  666. expect { ticket.destroy }
  667. .to change { customer.reload.updated_at }
  668. .and change { organization.reload.updated_at }
  669. end
  670. end
  671. context 'when customer association is changed' do
  672. it 'touches both old and new customer, and their organizations' do
  673. expect { ticket.update(customer: other_customer) }
  674. .to change { customer.reload.updated_at }
  675. .and change { organization.reload.updated_at }
  676. .and change { other_customer.reload.updated_at }
  677. .and change { other_organization.reload.updated_at }
  678. end
  679. end
  680. end
  681. describe 'Association & attachment management:' do
  682. it 'deletes all related ActivityStreams on destroy' do
  683. create_list(:activity_stream, 3, o: ticket)
  684. expect { ticket.destroy }
  685. .to change { ActivityStream.exists?(activity_stream_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  686. .to(false)
  687. end
  688. it 'deletes all related Links on destroy' do
  689. create(:link, from: ticket, to: create(:ticket))
  690. create(:link, from: create(:ticket), to: ticket)
  691. create(:link, from: ticket, to: create(:ticket))
  692. expect { ticket.destroy }
  693. .to change { Link.where('link_object_source_value = :id OR link_object_target_value = :id', id: ticket.id).any? }
  694. .to(false)
  695. end
  696. it 'deletes all related Articles on destroy' do
  697. create_list(:ticket_article, 3, ticket: ticket)
  698. expect { ticket.destroy }
  699. .to change { Ticket::Article.exists?(ticket: ticket) }
  700. .to(false)
  701. end
  702. it 'deletes all related OnlineNotifications on destroy' do
  703. create_list(:online_notification, 3, o: ticket)
  704. expect { ticket.destroy }
  705. .to change { OnlineNotification.where(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id).any? }
  706. .to(false)
  707. end
  708. it 'deletes all related Tags on destroy' do
  709. create_list(:tag, 3, o: ticket)
  710. expect { ticket.destroy }
  711. .to change { Tag.exists?(tag_object_id: Tag::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  712. .to(false)
  713. end
  714. it 'deletes all related Histories on destroy' do
  715. create_list(:history, 3, o: ticket)
  716. expect { ticket.destroy }
  717. .to change { History.exists?(history_object_id: History::Object.lookup(name: 'Ticket').id, o_id: ticket.id) }
  718. .to(false)
  719. end
  720. it 'deletes all related Karma::ActivityLogs on destroy' do
  721. create_list(:'karma/activity_log', 3, o: ticket)
  722. expect { ticket.destroy }
  723. .to change { Karma::ActivityLog.exists?(object_lookup_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  724. .to(false)
  725. end
  726. it 'deletes all related RecentViews on destroy' do
  727. create_list(:recent_view, 3, o: ticket)
  728. expect { ticket.destroy }
  729. .to change { RecentView.exists?(recent_view_object_id: ObjectLookup.by_name('Ticket'), o_id: ticket.id) }
  730. .to(false)
  731. end
  732. context 'when ticket is generated from email (with attachments)' do
  733. subject(:ticket) { Channel::EmailParser.new.process({}, raw_email).first }
  734. let(:raw_email) { File.read(Rails.root.join('test/data/mail/mail001.box')) }
  735. it 'adds attachments to the Store{::File,::Provider::DB} tables' do
  736. expect { ticket }
  737. .to change(Store, :count).by(2)
  738. .and change { Store::File.count }.by(2)
  739. .and change { Store::Provider::DB.count }.by(2)
  740. end
  741. context 'and subsequently destroyed' do
  742. it 'deletes all related attachments' do
  743. ticket # create ticket
  744. expect { ticket.destroy }
  745. .to change(Store, :count).by(-2)
  746. .and change { Store::File.count }.by(-2)
  747. .and change { Store::Provider::DB.count }.by(-2)
  748. end
  749. end
  750. context 'and a duplicate ticket is generated from the same email' do
  751. before { ticket } # create ticket
  752. let(:duplicate) { Channel::EmailParser.new.process({}, raw_email).first }
  753. it 'adds duplicate attachments to the Store table only' do
  754. expect { duplicate }
  755. .to change(Store, :count).by(2)
  756. .and change { Store::File.count }.by(0)
  757. .and change { Store::Provider::DB.count }.by(0)
  758. end
  759. context 'when only the duplicate ticket is destroyed' do
  760. it 'deletes only the duplicate attachments' do
  761. duplicate # create ticket
  762. expect { duplicate.destroy }
  763. .to change(Store, :count).by(-2)
  764. .and change { Store::File.count }.by(0)
  765. .and change { Store::Provider::DB.count }.by(0)
  766. end
  767. it 'deletes all related attachments' do
  768. duplicate.destroy
  769. expect { ticket.destroy }
  770. .to change(Store, :count).by(-2)
  771. .and change { Store::File.count }.by(-2)
  772. .and change { Store::Provider::DB.count }.by(-2)
  773. end
  774. end
  775. end
  776. end
  777. end
  778. end
  779. end