ticket_biz_break_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. RSpec.describe Escalation::TicketBizBreak, time_zone: 'Europe/Berlin' do
  4. let(:ticket) { create(:ticket) }
  5. let(:calendar) { create(:calendar) }
  6. let(:instance) { described_class.new(ticket, calendar) }
  7. describe '#biz_breaks' do
  8. let(:result) { instance.biz_breaks }
  9. context 'when ticket history is empty' do
  10. it { expect(result).to be_a(Hash) }
  11. it { expect(result).to be_empty }
  12. end
  13. context 'when ticket is opened' do
  14. before do
  15. travel 15.minutes
  16. ticket.update! state: Ticket::State.lookup(name: 'open')
  17. end
  18. it { expect(result).to be_a(Hash) }
  19. it { expect(result).to be_empty }
  20. end
  21. context 'when ticket was opened and closed' do
  22. before do
  23. travel 15.minutes
  24. ticket.update! state: Ticket::State.lookup(name: 'open')
  25. travel 15.minutes
  26. ticket.update! state: Ticket::State.lookup(name: 'closed')
  27. end
  28. it { expect(result).to be_a(Hash) }
  29. it { expect(result).to be_empty }
  30. end
  31. context 'when ticket was started in non-escalated state and closed' do
  32. let(:ticket) { create(:ticket, state: Ticket::State.lookup(name: 'pending reminder')) }
  33. before do
  34. travel_to Time.current.noon
  35. ticket
  36. travel 15.minutes
  37. ticket.update! state: Ticket::State.lookup(name: 'closed')
  38. end
  39. it { expect(result).to be_a(Hash) }
  40. it { expect(result).to be_one }
  41. end
  42. context 'when ticket was suspended and reopened multiple times' do
  43. before do
  44. travel_to Time.current.noon
  45. ticket.update! state: Ticket::State.lookup(name: 'open')
  46. travel 15.minutes
  47. ticket.update! state: Ticket::State.lookup(name: 'pending reminder')
  48. travel 15.minutes
  49. ticket.update! state: Ticket::State.lookup(name: 'open')
  50. travel 15.minutes
  51. ticket.update! state: Ticket::State.lookup(name: 'pending close')
  52. travel 15.minutes
  53. ticket.update! state: Ticket::State.lookup(name: 'closed')
  54. end
  55. let(:first_value) { result.values.first }
  56. it { expect(result).to be_a(Hash) }
  57. it { expect(result.keys).to be_one }
  58. it { expect(result.keys.first).to be_a(Date) }
  59. it { expect(result.keys.first).to eq(Time.current.to_date) }
  60. it { expect(first_value).to be_a(Hash) }
  61. it { expect(first_value.keys).to eq %w[12:15 12:45] }
  62. it { expect(first_value['12:15']).to eq '12:30' }
  63. it { expect(first_value['12:45']).to eq '13:00' }
  64. end
  65. context 'when ticket was suspended over midnight in UTC', time_zone: 'UTC' do
  66. before do
  67. travel_to Time.current.change(month: 11).utc.midnight
  68. travel(-15.minutes)
  69. ticket.update! state: Ticket::State.lookup(name: 'pending reminder')
  70. travel 30.minutes
  71. ticket.update! state: Ticket::State.lookup(name: 'open')
  72. end
  73. let(:first_value) { result.values.first }
  74. it { expect(result.keys).to be_one }
  75. it { expect(result.keys.first).to be_a(Date) }
  76. it { expect(result.keys.first).to eq(Time.current.to_date) }
  77. it { expect(first_value).to be_a(Hash) }
  78. it { expect(first_value.keys).to eq %w[00:45] }
  79. it { expect(first_value['00:45']).to eq '01:15' }
  80. end
  81. context 'when ticket was suspended over midnight in timezone' do
  82. before do
  83. travel_to Time.current.midnight
  84. travel(-15.minutes)
  85. ticket.update! state: Ticket::State.lookup(name: 'pending reminder')
  86. travel 1.hour
  87. ticket.update! state: Ticket::State.lookup(name: 'open')
  88. end
  89. let(:first_value) { result.values.first }
  90. let(:second_value) { result.values.second }
  91. it { expect(result.keys.count).to be(2) }
  92. it { expect(result.keys).to eq [Time.current.yesterday.to_date, Time.current.to_date] }
  93. it { expect(first_value).to be_a(Hash) }
  94. it { expect(first_value.keys).to eq %w[23:45] }
  95. it { expect(first_value['23:45']).to eq '24:00' }
  96. it { expect(second_value).to be_a(Hash) }
  97. it { expect(second_value.keys).to eq %w[00:00] }
  98. it { expect(second_value['00:00']).to eq '00:45' }
  99. end
  100. context 'when ticket was suspended for multiple days' do
  101. before do
  102. travel_to Time.current.noon
  103. ticket.update! state: Ticket::State.lookup(name: 'pending reminder')
  104. travel 5.days
  105. ticket.update! state: Ticket::State.lookup(name: 'open')
  106. end
  107. let(:first_value) { result.values.first }
  108. let(:second_value) { result.values.second }
  109. it { expect(result.keys.count).to be(6) }
  110. it { expect(result.keys).to eq (5.days.ago.to_date..Time.current).to_a }
  111. it { expect(result.values[0].keys).to eq %w[12:00] }
  112. it { expect(result.values[0]['12:00']).to eq '24:00' }
  113. it { expect(result.values[1].keys).to eq %w[00:00] }
  114. it { expect(result.values[1]['00:00']).to eq '24:00' }
  115. it { expect(result.values[2].keys).to eq %w[00:00] }
  116. it { expect(result.values[2]['00:00']).to eq '24:00' }
  117. it { expect(result.values[3].keys).to eq %w[00:00] }
  118. it { expect(result.values[3]['00:00']).to eq '24:00' }
  119. it { expect(result.values[4].keys).to eq %w[00:00] }
  120. it { expect(result.values[4]['00:00']).to eq '24:00' }
  121. it { expect(result.values[5].keys).to eq %w[00:00] }
  122. it { expect(result.values[5]['00:00']).to eq '12:00' }
  123. end
  124. end
  125. describe '#history_list_states' do
  126. let(:result) { instance.send(:history_list_states) }
  127. it 'empty when history log is empty' do
  128. expect(result).to be_empty
  129. end
  130. it 'empty when history log has non-state records' do
  131. ticket.update! title: '2nd title'
  132. expect(result).to be_empty
  133. end
  134. it 'returns array of Hashes when history log has various records' do
  135. ticket.update! title: '2nd title', state: Ticket::State.lookup(name: 'open')
  136. expect(result.first).to be_a Hash
  137. end
  138. it 'lists changes in specific order when history log has various records' do
  139. ticket.update! title: 'title', state: Ticket::State.lookup(name: 'open')
  140. ticket.update! title: 'another title'
  141. ticket.update! state: Ticket::State.lookup(name: 'closed')
  142. expect(result.pluck('value_to')).to eq %w[open closed]
  143. end
  144. end
  145. describe '#ignored_escalation_state_names' do
  146. let(:result) { instance.send(:ignored_escalation_state_names) }
  147. it { expect(result).to be_a Array }
  148. it { expect(result).to be_all String }
  149. it { expect(result).to include 'closed' }
  150. it { expect(result).not_to include 'open' }
  151. end
  152. describe '#history_list_in_break' do
  153. let(:result) { instance.send(:history_list_in_break) }
  154. it { expect(result).to be_a Array }
  155. it 'empty history returns minutes in timeframe' do
  156. expect(result).to be_empty
  157. end
  158. context 'when contains 4 history points' do
  159. before do
  160. allow(instance).to receive(:history_list_states).and_return(history_list_4)
  161. end
  162. let(:history_list_4) do
  163. [
  164. mock_state_hash(ticket.created_at, nil, 'new'),
  165. mock_state_hash(ticket.created_at + 1.hour, 'new', 'open'),
  166. mock_state_hash(ticket.created_at + 90.minutes, 'open', 'pending close'),
  167. mock_state_hash(ticket.created_at + 2.hours, 'pending close', 'closed')
  168. ]
  169. end
  170. it 'returns one range' do
  171. expect(result).to be_one
  172. end
  173. it 'returns range from pending close' do
  174. expect(result.first.first['value_to']).to eq 'pending close'
  175. end
  176. it 'returns range to closed' do
  177. expect(result.first.second['value_to']).to eq 'closed'
  178. end
  179. it 'calls #range_on_break? thrice' do
  180. allow(instance).to receive(:range_on_break?)
  181. result
  182. expect(instance).to have_received(:range_on_break?).exactly(3).times
  183. end
  184. end
  185. end
  186. describe '#accumulate_breaks' do
  187. let(:input_a) { { Time.current.to_date => { '10:00' => '14:00' }, Time.current.tomorrow.to_date => { '10:00' => '14:05' } } }
  188. let(:input_b) { { Time.current.to_date => { '17:00' => '18:00' } } }
  189. let(:result) { instance.send(:accumulate_breaks, [input_a, input_b]) }
  190. it { expect(result.keys).to eq [Time.current.to_date, Time.current.tomorrow.to_date] }
  191. it { expect(result[Time.current.to_date]).to eq({ '10:00' => '14:00', '17:00' => '18:00' }) }
  192. it { expect(result[Time.current.tomorrow.to_date]).to eq({ '10:00' => '14:05' }) }
  193. end
  194. describe '#history_range_to_breaks' do
  195. before { travel_to Time.current.noon }
  196. let(:result) { instance.send(:history_range_to_breaks, history_from, history_to) }
  197. context 'when fits in a single day' do
  198. let(:history_from) { mock_state_hash(ticket.created_at + 90.minutes, 'open', 'pending close') }
  199. let(:history_to) { mock_state_hash(ticket.created_at + 2.hours, 'pending close', 'closed') }
  200. it { expect(result).to be_a Hash }
  201. it { expect(result.keys).to eq [Time.current.to_date] }
  202. it { expect(result.values.first).to eq({ '13:30' => '14:00' }) }
  203. end
  204. context 'when spills over to multiple days' do
  205. let(:history_from) { mock_state_hash(ticket.created_at + 90.minutes, 'open', 'pending close') }
  206. let(:history_to) { mock_state_hash(ticket.created_at + 2.days, 'pending close', 'closed') }
  207. it { expect(result).to be_a Hash }
  208. it { expect(result.keys).to eq [Time.current.to_date, Time.current.tomorrow.to_date, 2.days.from_now.to_date] }
  209. it { expect(result.values.first).to eq({ '13:30' => '24:00' }) }
  210. it { expect(result.values.second).to eq({ '00:00' => '24:00' }) }
  211. it { expect(result.values.third).to eq({ '00:00' => '12:00' }) }
  212. end
  213. end
  214. describe '#mock_initial_state' do
  215. let(:result) { instance.send(:mock_initial_state) }
  216. it { expect(result).to have_key('created_at').and(have_key('value_to')) }
  217. context 'when ticket has no history' do
  218. it { expect(result).to include('created_at' => ticket.created_at) }
  219. it { expect(result).to include('value_to' => ticket.state.name) }
  220. end
  221. shared_context 'when ticket has state changes' do
  222. let(:initial_state_name) { 'pending reminder' }
  223. let(:ticket) { create(:ticket, state: Ticket::State.lookup(name: initial_state_name)) }
  224. before do
  225. freeze_time
  226. ticket
  227. travel timespan
  228. ticket.update! state: Ticket::State.lookup(name: 'closed')
  229. end
  230. end
  231. context 'when ticket has state changes later' do
  232. let(:timespan) { 30.minutes }
  233. include_examples 'when ticket has state changes'
  234. it { expect(result).to include('created_at' => ticket.created_at) }
  235. it { expect(result).to include('value_to' => initial_state_name) }
  236. end
  237. context 'when ticket has state changes at creation' do
  238. let(:timespan) { 0.minutes }
  239. include_examples 'when ticket has state changes'
  240. it { expect(result).to be_nil }
  241. end
  242. end
  243. def mock_state_hash(time, from, to)
  244. {
  245. 'attribute' => 'state',
  246. 'created_at' => time,
  247. 'value_from' => from,
  248. 'value_to' => to
  249. }
  250. end
  251. end