calendar_spec.rb 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. require 'rails_helper'
  3. # Without `export TZ="Europe/London"` in the environment, the tests will fail. :(
  4. RSpec.describe Calendar, type: :model do
  5. subject(:calendar) { create(:calendar) }
  6. let(:feed) { Digest::MD5.hexdigest(calendar.ical_url) }
  7. describe 'attributes' do
  8. describe '#default' do
  9. before { expect(described_class.pluck(:default)).to eq([true]) }
  10. context 'when set to true on creation' do
  11. subject(:calendar) { build(:calendar, default: true) }
  12. it 'stays true and sets all other calendars to default: false' do
  13. expect { calendar.tap(&:save).reload }.not_to change(calendar, :default)
  14. expect(described_class.where(default: true) - [calendar]).to be_empty
  15. end
  16. end
  17. context 'when set to true on update' do
  18. subject(:calendar) { create(:calendar, default: false) }
  19. before { calendar.default = true }
  20. it 'stays true and sets all other calendars to default: false' do
  21. expect { calendar.tap(&:save).reload }.not_to change(calendar, :default)
  22. expect(described_class.where(default: true) - [calendar]).to be_empty
  23. end
  24. end
  25. context 'when set to false on update' do
  26. it 'sets default: true on earliest-created calendar' do
  27. expect { described_class.first.update(default: false) }
  28. .not_to change { described_class.first.default }
  29. end
  30. end
  31. context 'when default calendar is destroyed' do
  32. subject!(:calendar) { create(:calendar, default: false) }
  33. it 'sets default: true on earliest-created remaining calendar' do
  34. expect { described_class.first.destroy }
  35. .to change { calendar.reload.default }.to(true)
  36. end
  37. context 'when sla has destroyed calendar set' do
  38. let(:sla) { create(:sla, calendar: described_class.first) }
  39. before do
  40. sla
  41. end
  42. it 'sets the new default calendar to the sla' do
  43. expect { described_class.first.destroy }
  44. .to change { sla.reload.calendar }.to(calendar)
  45. end
  46. end
  47. end
  48. end
  49. describe '#public_holidays' do
  50. subject(:calendar) do
  51. create(:calendar, ical_url: Rails.root.join('test/data/calendar/calendar1.ics'))
  52. end
  53. before { travel_to Time.zone.parse('2017-08-24T01:04:44Z0') }
  54. context 'on creation' do
  55. it 'is computed from iCal event data (implicitly via #sync), from one year before to three years after' do
  56. expect(calendar.public_holidays).to eq(
  57. '2016-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  58. '2017-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  59. '2018-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  60. '2019-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  61. )
  62. end
  63. context 'with one-time and n-time (recurring) events' do
  64. subject(:calendar) do
  65. create(:calendar, ical_url: Rails.root.join('test/data/calendar/calendar3.ics'))
  66. end
  67. it 'accurately computes/imports events' do
  68. expect(calendar.public_holidays).to eq(
  69. '2016-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  70. '2016-12-26' => { 'active' => true, 'summary' => 'day3', 'feed' => feed },
  71. '2016-12-28' => { 'active' => true, 'summary' => 'day5', 'feed' => feed },
  72. '2017-01-26' => { 'active' => true, 'summary' => 'day3', 'feed' => feed },
  73. '2017-02-26' => { 'active' => true, 'summary' => 'day3', 'feed' => feed },
  74. '2017-03-26' => { 'active' => true, 'summary' => 'day3', 'feed' => feed },
  75. '2017-04-25' => { 'active' => true, 'summary' => 'day3', 'feed' => feed },
  76. '2017-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  77. '2018-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  78. '2019-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  79. )
  80. end
  81. end
  82. end
  83. end
  84. end
  85. describe '#sync' do
  86. subject(:calendar) do
  87. create(:calendar, ical_url: Rails.root.join('test/data/calendar/calendar1.ics'), default: false)
  88. end
  89. before { travel_to Time.zone.parse('2017-08-24T01:04:44Z0') }
  90. context 'when called explicitly after creation' do
  91. it 'writes #public_holidays to the cache (valid for 1 day)' do
  92. expect(Rails.cache.read("CalendarIcal::#{calendar.id}")).to be_nil
  93. expect { calendar.sync }
  94. .to change { Rails.cache.read("CalendarIcal::#{calendar.id}") }
  95. .to(calendar.attributes.slice('public_holidays', 'ical_url').symbolize_keys)
  96. end
  97. context 'and neither current date nor iCal URL have changed' do
  98. it 'is idempotent' do
  99. expect { calendar.sync }
  100. .not_to change(calendar, :public_holidays)
  101. end
  102. it 'does not create a background job for escalation rebuild' do
  103. calendar # create and sync (1 inital background job is created)
  104. expect { calendar.sync } # a second sync right after calendar create
  105. .to not_change { Delayed::Job.count }
  106. end
  107. end
  108. context 'and current date has changed but neither public_holidays nor iCal URL have changed (past cache expiry)' do
  109. before do
  110. calendar # create and sync
  111. travel 2.days
  112. end
  113. it 'is idempotent' do
  114. expect { calendar.sync }
  115. .not_to change(calendar, :public_holidays)
  116. end
  117. it 'does not create a background job for escalation rebuild' do
  118. expect { calendar.sync }
  119. .not_to change(Delayed::Job, :count)
  120. end
  121. end
  122. context 'and current date has changed (past cache expiry)', performs_jobs: true do
  123. before do
  124. calendar # create and sync
  125. clear_jobs # clear (speak: process) created jobs
  126. travel 1.year
  127. end
  128. it 'appends newly computed event data to #public_holidays' do
  129. expect { calendar.sync }.to change(calendar, :public_holidays).to(
  130. '2016-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  131. '2017-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  132. '2018-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  133. '2019-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  134. '2020-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  135. )
  136. end
  137. it 'does create a background job for escalation rebuild' do
  138. expect { calendar.sync }.to have_enqueued_job(TicketEscalationRebuildJob)
  139. end
  140. end
  141. context 'and iCal URL has changed' do
  142. before { calendar.assign_attributes(ical_url: Rails.root.join('test/data/calendar/calendar2.ics')) }
  143. it 'replaces #public_holidays with event data computed from new iCal URL' do
  144. expect { calendar.save }
  145. .to change(calendar, :public_holidays).to(
  146. '2016-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  147. '2016-12-25' => { 'active' => true, 'summary' => 'Christmas2', 'feed' => feed },
  148. '2017-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  149. '2017-12-25' => { 'active' => true, 'summary' => 'Christmas2', 'feed' => feed },
  150. '2018-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  151. '2018-12-25' => { 'active' => true, 'summary' => 'Christmas2', 'feed' => feed },
  152. '2019-12-24' => { 'active' => true, 'summary' => 'Christmas1', 'feed' => feed },
  153. '2019-12-25' => { 'active' => true, 'summary' => 'Christmas2', 'feed' => feed },
  154. )
  155. end
  156. end
  157. context 'when verifying that no duplicate events are synced' do
  158. before do
  159. calendar.assign_attributes(ical_url: Rails.root.join('test/data/calendar/calendar_duplicate_check.ics'))
  160. calendar.sync
  161. end
  162. it 'does not create duplicate events' do
  163. expect(calendar.public_holidays).to eq(
  164. '2019-01-01' => { 'active' => true, 'feed' => feed, 'summary' => 'Neujahrstag' },
  165. '2019-04-22' => { 'active' => true, 'feed' => feed, 'summary' => 'Ostermontag' },
  166. )
  167. end
  168. end
  169. end
  170. end
  171. describe '#validate_hours' do
  172. context 'when business_hours are invalid' do
  173. it 'fails for hours ending at 00:00' do
  174. expect do
  175. create(:calendar,
  176. business_hours: {
  177. mon: {
  178. active: true,
  179. timeframes: [['09:00', '00:00']]
  180. },
  181. tue: {
  182. active: true,
  183. timeframes: [['09:00', '00:00']]
  184. },
  185. wed: {
  186. active: true,
  187. timeframes: [['09:00', '00:00']]
  188. },
  189. thu: {
  190. active: true,
  191. timeframes: [['09:00', '00:00']]
  192. },
  193. fri: {
  194. active: true,
  195. timeframes: [['09:00', '00:00']]
  196. },
  197. sat: {
  198. active: false,
  199. timeframes: [['09:00', '00:00']]
  200. },
  201. sun: {
  202. active: false,
  203. timeframes: [['09:00', '00:00']]
  204. }
  205. })
  206. end.to raise_error(ActiveRecord::RecordInvalid, %r{nonsensical hours provided})
  207. end
  208. it 'fails for blank structure' do
  209. expect do
  210. create(:calendar,
  211. business_hours: {})
  212. end.to raise_error(ActiveRecord::RecordInvalid, %r{There are no business hours configured.})
  213. end
  214. end
  215. end
  216. describe '#biz' do
  217. it 'overnight minutes are counted correctly' do
  218. travel_to Time.current.noon
  219. calendar = create(:calendar, '23:59/7')
  220. biz = calendar.biz
  221. expect(biz.time(24, :hours).after(Time.current)).to eq 1.day.from_now
  222. end
  223. end
  224. describe '#business_hours_to_hash' do
  225. it 'returns a hash with all weekdays' do
  226. calendar = create(:calendar, '23:59/7')
  227. hash = calendar.business_hours_to_hash
  228. expect(hash.keys).to eq %i[mon tue wed thu fri sat sun]
  229. end
  230. context 'with mocked hours' do
  231. let(:calendar) { create(:calendar, '23:59/7') }
  232. let(:result) { calendar.business_hours_to_hash }
  233. before do
  234. calendar.business_hours = {
  235. day_1: { active: true, timeframes: [['09:00', '17:00']] },
  236. day_2: { active: true, timeframes: [['00:01', '02:00'], ['09:00', '17:00']] },
  237. day_3: { active: false, timeframes: [['09:00', '17:00']] }
  238. }
  239. end
  240. it { expect(result.keys).to eq %i[day_1 day_2] }
  241. it { expect(result[:day_1]).to eq({ '09:00' => '17:00' }) }
  242. it { expect(result[:day_2]).to eq({ '09:00' => '17:00', '00:01' => '02:00' }) }
  243. end
  244. end
  245. context 'when updated Calendar no longer matches Ticket', :performs_jobs do
  246. subject(:ticket) { create(:ticket, created_at: '2016-11-01 13:56:21 UTC', updated_at: '2016-11-01 13:56:21 UTC') }
  247. let(:calendar) do
  248. create(:calendar,
  249. business_hours: {
  250. mon: {
  251. active: true,
  252. timeframes: [ ['08:00', '20:00'] ]
  253. },
  254. tue: {
  255. active: true,
  256. timeframes: [ ['08:00', '20:00'] ]
  257. },
  258. wed: {
  259. active: true,
  260. timeframes: [ ['08:00', '20:00'] ]
  261. },
  262. thu: {
  263. active: true,
  264. timeframes: [ ['08:00', '20:00'] ]
  265. },
  266. fri: {
  267. active: true,
  268. timeframes: [ ['08:00', '20:00'] ]
  269. },
  270. sat: {
  271. active: false,
  272. timeframes: [ ['08:00', '17:00'] ]
  273. },
  274. sun: {
  275. active: false,
  276. timeframes: [ ['08:00', '17:00'] ]
  277. },
  278. },
  279. public_holidays: {
  280. '2016-11-01' => {
  281. 'active' => true,
  282. 'summary' => 'test 1',
  283. },
  284. })
  285. end
  286. let(:sla) { create(:sla, condition: {}, calendar: calendar, first_response_time: 60, response_time: 120, solution_time: nil) }
  287. before do
  288. queue_adapter.perform_enqueued_jobs = true
  289. queue_adapter.perform_enqueued_at_jobs = true
  290. sla
  291. ticket
  292. create(:'ticket/article', :inbound_web, ticket: ticket, created_at: '2016-11-01 13:56:21 UTC', updated_at: '2016-11-01 13:56:21 UTC')
  293. ticket.reload
  294. create(:'ticket/article', :outbound_email, ticket: ticket, created_at: '2016-11-07 13:26:36 UTC', updated_at: '2016-11-07 13:26:36 UTC')
  295. ticket.reload
  296. end
  297. it 'calculates escalation_at attributes' do
  298. expect(ticket.escalation_at).to be_nil
  299. expect(ticket.first_response_escalation_at).to be_nil
  300. expect(ticket.update_escalation_at).to be_nil
  301. expect(ticket.close_escalation_at).to be_nil
  302. # set sla's for timezone "Europe/Berlin" wintertime (+1), so UTC times are 3:00-18:00
  303. calendar.update!(
  304. business_hours: {
  305. mon: {
  306. active: true,
  307. timeframes: [ ['04:00', '20:00'] ]
  308. },
  309. tue: {
  310. active: true,
  311. timeframes: [ ['04:00', '20:00'] ]
  312. },
  313. wed: {
  314. active: true,
  315. timeframes: [ ['04:00', '20:00'] ]
  316. },
  317. thu: {
  318. active: true,
  319. timeframes: [ ['04:00', '20:00'] ]
  320. },
  321. fri: {
  322. active: true,
  323. timeframes: [ ['04:00', '20:00'] ]
  324. },
  325. sat: {
  326. active: false,
  327. timeframes: [ ['04:00', '13:00'] ] # this changed from '17:00' => '13:00'
  328. },
  329. sun: {
  330. active: false,
  331. timeframes: [ ['04:00', '17:00'] ]
  332. },
  333. },
  334. public_holidays: {
  335. '2016-11-01' => {
  336. 'active' => true,
  337. 'summary' => 'test 1',
  338. },
  339. },
  340. )
  341. ticket.reload
  342. expect(ticket.escalation_at).to be_nil
  343. expect(ticket.first_response_escalation_at).to be_nil
  344. expect(ticket.update_escalation_at).to be_nil
  345. expect(ticket.close_escalation_at).to be_nil
  346. end
  347. end
  348. context 'when SLA relevant timezone holidays are configured' do
  349. let(:calendar) do
  350. create(:calendar,
  351. public_holidays: {
  352. '2015-09-22' => {
  353. 'active' => true,
  354. 'summary' => 'test 1',
  355. },
  356. '2015-09-23' => {
  357. 'active' => false,
  358. 'summary' => 'test 2',
  359. },
  360. '2015-09-24' => {
  361. 'removed' => false,
  362. 'summary' => 'test 3',
  363. },
  364. })
  365. end
  366. let(:sla) do
  367. create(:sla,
  368. calendar: calendar,
  369. condition: {},
  370. first_response_time: 120,
  371. response_time: 180,
  372. solution_time: 240)
  373. end
  374. before do
  375. sla
  376. ticket.reload
  377. end
  378. context 'when a Ticket is created in working hours but not affected by the configured holidays' do
  379. subject(:ticket) { create(:ticket, created_at: '2013-10-21 09:30:00 UTC', updated_at: '2013-10-21 09:30:00 UTC') }
  380. it 'calculates escalation_at attributes' do
  381. expect(ticket.escalation_at.gmtime.to_s).to eq('2013-10-21 11:30:00 UTC')
  382. expect(ticket.first_response_escalation_at.gmtime.to_s).to eq('2013-10-21 11:30:00 UTC')
  383. expect(ticket.update_escalation_at).to be_nil
  384. expect(ticket.close_escalation_at.gmtime.to_s).to eq('2013-10-21 13:30:00 UTC')
  385. end
  386. end
  387. context 'when a Ticket is created before the working hours but not affected by the configured holidays' do
  388. subject(:ticket) { create(:ticket, created_at: '2013-10-21 05:30:00 UTC', updated_at: '2013-10-21 05:30:00 UTC') }
  389. it 'calculates escalation_at attributes' do
  390. expect(ticket.escalation_at.gmtime.to_s).to eq('2013-10-21 09:00:00 UTC')
  391. expect(ticket.first_response_escalation_at.gmtime.to_s).to eq('2013-10-21 09:00:00 UTC')
  392. expect(ticket.update_escalation_at).to be_nil
  393. expect(ticket.close_escalation_at.gmtime.to_s).to eq('2013-10-21 11:00:00 UTC')
  394. end
  395. end
  396. context 'when a Ticket is created before the holidays but escalation should take place while holidays are' do
  397. subject(:ticket) { create(:ticket, created_at: '2015-09-21 14:30:00 UTC', updated_at: '2015-09-21 14:30:00 UTC') }
  398. it 'calculates escalation_at attributes' do
  399. expect(ticket.escalation_at.gmtime.to_s).to eq('2015-09-23 08:30:00 UTC')
  400. expect(ticket.first_response_escalation_at.gmtime.to_s).to eq('2015-09-23 08:30:00 UTC')
  401. expect(ticket.update_escalation_at).to be_nil
  402. expect(ticket.close_escalation_at.gmtime.to_s).to eq('2015-09-23 10:30:00 UTC')
  403. end
  404. end
  405. end
  406. describe '.timezones' do
  407. it 'includes known zones' do
  408. expect(described_class.timezones.keys).to include('GMT', 'Europe/Berlin', 'Atlantic/Reykjavik')
  409. end
  410. end
  411. end