cronofy 0.0.5 → 0.37.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3712 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Cronofy::Client do
4
+ before(:all) do
5
+ WebMock.reset!
6
+ WebMock.disable_net_connect!(allow_localhost: true)
7
+ end
8
+
9
+ let(:token) { 'token_123' }
10
+ let(:base_request_headers) do
11
+ {
12
+ "Authorization" => "Bearer #{token}",
13
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
14
+ }
15
+ end
16
+
17
+ let(:json_request_headers) do
18
+ base_request_headers.merge("Content-Type" => "application/json; charset=utf-8")
19
+ end
20
+
21
+ let(:request_headers) do
22
+ base_request_headers
23
+ end
24
+
25
+ let(:request_body) { nil }
26
+
27
+ let(:client) do
28
+ Cronofy::Client.new(
29
+ client_id: 'client_id_123',
30
+ client_secret: 'client_secret_456',
31
+ access_token: token,
32
+ refresh_token: 'refresh_token_456',
33
+ )
34
+ end
35
+
36
+ let(:correct_response_headers) do
37
+ { 'Content-Type' => 'application/json; charset=utf-8' }
38
+ end
39
+
40
+ shared_examples 'a Cronofy request with mapped return value' do
41
+ it 'returns the correct response when no error' do
42
+ stub_request(method, request_url)
43
+ .with(headers: request_headers,
44
+ body: request_body)
45
+ .to_return(status: correct_response_code,
46
+ headers: correct_response_headers,
47
+ body: correct_response_body.to_json)
48
+
49
+ expect(subject).to eq correct_mapped_result
50
+ end
51
+ end
52
+
53
+ shared_examples 'a Cronofy request' do
54
+ it "doesn't raise an error when response is correct" do
55
+ stub_request(method, request_url)
56
+ .with(headers: request_headers,
57
+ body: request_body)
58
+ .to_return(status: correct_response_code,
59
+ headers: correct_response_headers,
60
+ body: correct_response_body.to_json)
61
+
62
+ subject
63
+ end
64
+
65
+ it 'raises AuthenticationFailureError on 401s' do
66
+ stub_request(method, request_url)
67
+ .with(headers: request_headers,
68
+ body: request_body)
69
+ .to_return(status: 401,
70
+ headers: correct_response_headers,
71
+ body: correct_response_body.to_json)
72
+ expect{ subject }.to raise_error(Cronofy::AuthenticationFailureError)
73
+ end
74
+
75
+ it 'raises AuthorizationFailureError on 403s' do
76
+ stub_request(method, request_url)
77
+ .with(headers: request_headers,
78
+ body: request_body)
79
+ .to_return(status: 403,
80
+ headers: correct_response_headers,
81
+ body: correct_response_body.to_json)
82
+ expect{ subject }.to raise_error(Cronofy::AuthorizationFailureError)
83
+ end
84
+
85
+ it 'raises NotFoundError on 404s' do
86
+ stub_request(method, request_url)
87
+ .with(headers: request_headers,
88
+ body: request_body)
89
+ .to_return(status: 404,
90
+ headers: correct_response_headers,
91
+ body: correct_response_body.to_json)
92
+ expect{ subject }.to raise_error(::Cronofy::NotFoundError)
93
+ end
94
+
95
+ it 'raises InvalidRequestError on 422s' do
96
+ stub_request(method, request_url)
97
+ .with(headers: request_headers,
98
+ body: request_body)
99
+ .to_return(status: 422,
100
+ headers: correct_response_headers,
101
+ body: correct_response_body.to_json)
102
+ expect{ subject }.to raise_error(::Cronofy::InvalidRequestError)
103
+ end
104
+
105
+ it 'raises AccountLockedError on 423s' do
106
+ stub_request(method, request_url)
107
+ .with(headers: request_headers,
108
+ body: request_body)
109
+ .to_return(status: 423,
110
+ headers: correct_response_headers,
111
+ body: correct_response_body.to_json)
112
+ expect{ subject }.to raise_error(::Cronofy::AccountLockedError)
113
+ end
114
+
115
+ it 'raises TooManyRequestsError on 429s' do
116
+ stub_request(method, request_url)
117
+ .with(headers: request_headers,
118
+ body: request_body)
119
+ .to_return(status: 429,
120
+ headers: correct_response_headers,
121
+ body: correct_response_body.to_json)
122
+ expect{ subject }.to raise_error(::Cronofy::TooManyRequestsError)
123
+ end
124
+
125
+ it 'raises ServerError on 500s' do
126
+ stub_request(method, request_url)
127
+ .with(headers: request_headers,
128
+ body: request_body)
129
+ .to_return(status: 500,
130
+ headers: correct_response_headers,
131
+ body: correct_response_body.to_json)
132
+ expect{ subject }.to raise_error(::Cronofy::ServerError)
133
+ end
134
+ end
135
+
136
+ describe '#create_calendar' do
137
+ let(:request_url) { 'https://api.cronofy.com/v1/calendars' }
138
+ let(:method) { :post }
139
+
140
+ let(:profile_id) { "pro_1234" }
141
+ let(:calendar_name) { "Home" }
142
+ let(:color) { "#49BED8" }
143
+
144
+ let(:correct_response_code) { 200 }
145
+ let(:correct_response_body) do
146
+ {
147
+ "calendar" => {
148
+ "provider_name" => "google",
149
+ "profile_name" => "example@cronofy.com",
150
+ "calendar_id" => "cal_n23kjnwrw2_jsdfjksn234",
151
+ "calendar_name" => "Home",
152
+ "calendar_readonly" => false,
153
+ "calendar_deleted" => false
154
+ }
155
+ }
156
+ end
157
+
158
+ let(:correct_mapped_result) do
159
+ Cronofy::Calendar.new(correct_response_body["calendar"])
160
+ end
161
+
162
+ context "with mandatory arguments" do
163
+ let(:request_body) do
164
+ {
165
+ profile_id: profile_id,
166
+ name: calendar_name,
167
+ }
168
+ end
169
+
170
+ subject { client.create_calendar(profile_id, calendar_name) }
171
+
172
+ it_behaves_like 'a Cronofy request'
173
+ it_behaves_like 'a Cronofy request with mapped return value'
174
+ end
175
+
176
+ context "with color" do
177
+ let(:request_body) do
178
+ {
179
+ profile_id: profile_id,
180
+ name: calendar_name,
181
+ color: color,
182
+ }
183
+ end
184
+
185
+ subject { client.create_calendar(profile_id, calendar_name, color: color) }
186
+
187
+ it_behaves_like 'a Cronofy request'
188
+ it_behaves_like 'a Cronofy request with mapped return value'
189
+ end
190
+ end
191
+
192
+ describe '#list_calendars' do
193
+ let(:request_url) { 'https://api.cronofy.com/v1/calendars' }
194
+ let(:method) { :get }
195
+ let(:correct_response_code) { 200 }
196
+ let(:correct_response_body) do
197
+ {
198
+ "calendars" => [
199
+ {
200
+ "provider_name" => "google",
201
+ "profile_name" => "example@cronofy.com",
202
+ "calendar_id" => "cal_n23kjnwrw2_jsdfjksn234",
203
+ "calendar_name" => "Home",
204
+ "calendar_readonly" => false,
205
+ "calendar_deleted" => false
206
+ },
207
+ {
208
+ "provider_name" => "google",
209
+ "profile_name" => "example@cronofy.com",
210
+ "calendar_id" => "cal_n23kjnwrw2_n1k323nkj23",
211
+ "calendar_name" => "Work",
212
+ "calendar_readonly" => true,
213
+ "calendar_deleted" => true
214
+ },
215
+ {
216
+ "provider_name" => "apple",
217
+ "profile_name" => "example@cronofy.com",
218
+ "calendar_id" => "cal_n23kjnwrw2_3nkj23wejk1",
219
+ "calendar_name" => "Bank Holidays",
220
+ "calendar_readonly" => true,
221
+ "calendar_deleted" => false
222
+ }
223
+ ]
224
+ }
225
+ end
226
+
227
+ let(:correct_mapped_result) do
228
+ correct_response_body["calendars"].map { |cal| Cronofy::Calendar.new(cal) }
229
+ end
230
+
231
+ subject { client.list_calendars }
232
+
233
+ it_behaves_like 'a Cronofy request'
234
+ it_behaves_like 'a Cronofy request with mapped return value'
235
+ end
236
+
237
+ describe 'Events' do
238
+ describe '#create_or_update_event' do
239
+ let(:calendar_id) { 'calendar_id_123'}
240
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events" }
241
+ let(:url) { URI("https://example.com") }
242
+ let(:method) { :post }
243
+ let(:request_headers) { json_request_headers }
244
+
245
+ let(:start_datetime) { Time.utc(2014, 8, 5, 15, 30, 0) }
246
+ let(:end_datetime) { Time.utc(2014, 8, 5, 17, 0, 0) }
247
+ let(:encoded_start_datetime) { "2014-08-05T15:30:00Z" }
248
+ let(:encoded_end_datetime) { "2014-08-05T17:00:00Z" }
249
+ let(:location) { { :description => "Board room" } }
250
+ let(:transparency) { nil }
251
+
252
+ let(:event) do
253
+ {
254
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
255
+ :summary => "Board meeting",
256
+ :description => "Discuss plans for the next quarter.",
257
+ :start => start_datetime,
258
+ :end => end_datetime,
259
+ :url => url,
260
+ :location => location,
261
+ :transparency => transparency,
262
+ :reminders => [
263
+ { :minutes => 60 },
264
+ { :minutes => 0 },
265
+ { :minutes => 10 },
266
+ ],
267
+ }
268
+ end
269
+
270
+ let(:request_body) do
271
+ {
272
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
273
+ :summary => "Board meeting",
274
+ :description => "Discuss plans for the next quarter.",
275
+ :start => encoded_start_datetime,
276
+ :end => encoded_end_datetime,
277
+ :url => url.to_s,
278
+ :location => location,
279
+ :transparency => transparency,
280
+ :reminders => [
281
+ { :minutes => 60 },
282
+ { :minutes => 0 },
283
+ { :minutes => 10 },
284
+ ],
285
+ }
286
+ end
287
+ let(:correct_response_code) { 202 }
288
+ let(:correct_response_body) { nil }
289
+
290
+ subject { client.create_or_update_event(calendar_id, event) }
291
+
292
+ context 'when start/end are Times' do
293
+ it_behaves_like 'a Cronofy request'
294
+ end
295
+
296
+ context 'when start/end are Dates' do
297
+ let(:start_datetime) { Date.new(2014, 8, 5) }
298
+ let(:end_datetime) { Date.new(2014, 8, 6) }
299
+ let(:encoded_start_datetime) { "2014-08-05" }
300
+ let(:encoded_end_datetime) { "2014-08-06" }
301
+
302
+ it_behaves_like 'a Cronofy request'
303
+ end
304
+
305
+ context 'when start/end are complex times' do
306
+ let(:start_datetime) do
307
+ {
308
+ :time => Time.utc(2014, 8, 5, 15, 30, 0),
309
+ :tzid => "Europe/London",
310
+ }
311
+ end
312
+ let(:end_datetime) do
313
+ {
314
+ :time => Time.utc(2014, 8, 5, 17, 0, 0),
315
+ :tzid => "America/Los_Angeles",
316
+ }
317
+ end
318
+ let(:encoded_start_datetime) do
319
+ {
320
+ :time => "2014-08-05T15:30:00Z",
321
+ :tzid => "Europe/London",
322
+ }
323
+ end
324
+ let(:encoded_end_datetime) do
325
+ {
326
+ :time => "2014-08-05T17:00:00Z",
327
+ :tzid => "America/Los_Angeles",
328
+ }
329
+ end
330
+
331
+ it_behaves_like 'a Cronofy request'
332
+ end
333
+
334
+ context 'when geo location present' do
335
+ let(:location) { { :description => "Board meeting", :lat => "1.2345", :long => "0.1234" } }
336
+
337
+ it_behaves_like 'a Cronofy request'
338
+ end
339
+
340
+ context 'when transparency present' do
341
+ let(:transparency) { "transparent" }
342
+
343
+ it_behaves_like 'a Cronofy request'
344
+ end
345
+
346
+ context 'when start and end already encoded' do
347
+ let(:start_datetime) { encoded_start_datetime }
348
+ let(:end_datetime) { encoded_end_datetime }
349
+
350
+ it_behaves_like 'a Cronofy request'
351
+ end
352
+ end
353
+
354
+ describe '#read_events' do
355
+ before do
356
+ stub_request(method, request_url)
357
+ .with(headers: request_headers,
358
+ body: request_body)
359
+ .to_return(status: correct_response_code,
360
+ headers: correct_response_headers,
361
+ body: correct_response_body.to_json)
362
+
363
+ stub_request(:get, next_page_url)
364
+ .with(headers: request_headers)
365
+ .to_return(status: correct_response_code,
366
+ headers: correct_response_headers,
367
+ body: next_page_body.to_json)
368
+ end
369
+
370
+
371
+ let(:request_url_prefix) { 'https://api.cronofy.com/v1/events' }
372
+ let(:method) { :get }
373
+ let(:correct_response_code) { 200 }
374
+ let(:next_page_url) do
375
+ "https://next.page.com/08a07b034306679e"
376
+ end
377
+
378
+ let(:params) { Hash.new }
379
+ let(:request_url) { request_url_prefix + "?tzid=Etc/UTC" }
380
+
381
+ let(:correct_response_body) do
382
+ {
383
+ 'pages' => {
384
+ 'current' => 1,
385
+ 'total' => 2,
386
+ 'next_page' => next_page_url
387
+ },
388
+ 'events' => [
389
+ {
390
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
391
+ 'event_uid' => 'evt_external_54008b1a4a41730f8d5c6037',
392
+ 'summary' => 'Company Retreat',
393
+ 'description' => '',
394
+ 'start' => '2014-09-06',
395
+ 'end' => '2014-09-08',
396
+ 'deleted' => false
397
+ },
398
+ {
399
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
400
+ 'event_uid' => 'evt_external_54008b1a4a41730f8d5c6038',
401
+ 'summary' => 'Dinner with Laura',
402
+ 'description' => '',
403
+ 'start' => '2014-09-13T19:00:00Z',
404
+ 'end' => '2014-09-13T21:00:00Z',
405
+ 'deleted' => false,
406
+ 'location' => {
407
+ 'description' => 'Pizzeria'
408
+ }
409
+ }
410
+ ]
411
+ }
412
+ end
413
+
414
+ let(:next_page_body) do
415
+ {
416
+ 'pages' => {
417
+ 'current' => 2,
418
+ 'total' => 2,
419
+ },
420
+ 'events' => [
421
+ {
422
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
423
+ 'event_uid' => 'evt_external_54008b1a4a4173023402934d',
424
+ 'summary' => 'Company Retreat Extended',
425
+ 'description' => '',
426
+ 'start' => '2014-09-06',
427
+ 'end' => '2014-09-08',
428
+ 'deleted' => false
429
+ },
430
+ {
431
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
432
+ 'event_uid' => 'evt_external_54008b1a4a41198273921312',
433
+ 'summary' => 'Dinner with Paul',
434
+ 'description' => '',
435
+ 'start' => '2014-09-13T19:00:00Z',
436
+ 'end' => '2014-09-13T21:00:00Z',
437
+ 'deleted' => false,
438
+ 'location' => {
439
+ 'description' => 'Cafe'
440
+ }
441
+ }
442
+ ]
443
+ }
444
+ end
445
+
446
+ let(:correct_mapped_result) do
447
+ first_page_events = correct_response_body['events'].map { |event| Cronofy::Event.new(event) }
448
+ second_page_events = next_page_body['events'].map { |event| Cronofy::Event.new(event) }
449
+
450
+ first_page_events + second_page_events
451
+ end
452
+
453
+ subject do
454
+ # By default force evaluation
455
+ client.read_events(params).to_a
456
+ end
457
+
458
+ context 'when all params are passed' do
459
+ let(:params) do
460
+ {
461
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
462
+ to: Time.new(2014, 10, 1, 0, 0, 1, '+00:00'),
463
+ tzid: 'Etc/UTC',
464
+ include_deleted: false,
465
+ include_moved: true,
466
+ last_modified: Time.new(2014, 8, 1, 0, 0, 1, '+00:00')
467
+ }
468
+ end
469
+ let(:request_url) do
470
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
471
+ "&to=2014-10-01T00:00:01Z&tzid=Etc/UTC&include_deleted=false" \
472
+ "&include_moved=true&last_modified=2014-08-01T00:00:01Z"
473
+ end
474
+
475
+ it_behaves_like 'a Cronofy request'
476
+ it_behaves_like 'a Cronofy request with mapped return value'
477
+ end
478
+
479
+ context 'when some params are passed' do
480
+ let(:params) do
481
+ {
482
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
483
+ include_deleted: false,
484
+ }
485
+ end
486
+ let(:request_url) do
487
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
488
+ "&tzid=Etc/UTC&include_deleted=false"
489
+ end
490
+
491
+ it_behaves_like 'a Cronofy request'
492
+ it_behaves_like 'a Cronofy request with mapped return value'
493
+ end
494
+
495
+ context "when unknown flags are passed" do
496
+ let(:params) do
497
+ {
498
+ unknown_bool: true,
499
+ unknown_number: 5,
500
+ unknown_string: "foo-bar-baz",
501
+ }
502
+ end
503
+
504
+ let(:request_url) do
505
+ "#{request_url_prefix}?tzid=Etc/UTC" \
506
+ "&unknown_bool=true" \
507
+ "&unknown_number=5" \
508
+ "&unknown_string=foo-bar-baz"
509
+ end
510
+
511
+ it_behaves_like 'a Cronofy request'
512
+ it_behaves_like 'a Cronofy request with mapped return value'
513
+ end
514
+
515
+ context "when calendar_ids are passed" do
516
+ let(:params) do
517
+ {
518
+ calendar_ids: ["cal_1234_abcd", "cal_1234_efgh", "cal_5678_ijkl"],
519
+ }
520
+ end
521
+
522
+ let(:request_url) do
523
+ "#{request_url_prefix}?tzid=Etc/UTC" \
524
+ "&calendar_ids[]=cal_1234_abcd" \
525
+ "&calendar_ids[]=cal_1234_efgh" \
526
+ "&calendar_ids[]=cal_5678_ijkl"
527
+ end
528
+
529
+ it_behaves_like 'a Cronofy request'
530
+ it_behaves_like 'a Cronofy request with mapped return value'
531
+ end
532
+
533
+ context "next page not found" do
534
+ before do
535
+ stub_request(:get, next_page_url)
536
+ .with(headers: request_headers)
537
+ .to_return(status: 404,
538
+ headers: correct_response_headers)
539
+ end
540
+
541
+ it "raises an error" do
542
+ expect{ subject }.to raise_error(::Cronofy::NotFoundError)
543
+ end
544
+ end
545
+
546
+ context "only first event" do
547
+ before do
548
+ # Ensure an error if second page is requested
549
+ stub_request(:get, next_page_url)
550
+ .with(headers: request_headers)
551
+ .to_return(status: 404,
552
+ headers: correct_response_headers)
553
+ end
554
+
555
+ let(:first_event) do
556
+ Cronofy::Event.new(correct_response_body["events"].first)
557
+ end
558
+
559
+ subject do
560
+ client.read_events(params).first
561
+ end
562
+
563
+ it "returns the first event from the first page" do
564
+ expect(subject).to eq(first_event)
565
+ end
566
+ end
567
+
568
+ context "without calling #to_a to force full evaluation" do
569
+ subject { client.read_events(params) }
570
+
571
+ it_behaves_like 'a Cronofy request'
572
+
573
+ # We expect it to behave like a Cronofy request as the first page is
574
+ # requested eagerly so that the majority of errors will happen inline
575
+ # rather than lazily happening wherever the iterator may have been
576
+ # passed.
577
+ end
578
+ end
579
+
580
+ describe '#delete_event' do
581
+ let(:calendar_id) { 'calendar_id_123'}
582
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events" }
583
+ let(:event_id) { 'event_id_456' }
584
+ let(:method) { :delete }
585
+ let(:request_headers) { json_request_headers }
586
+ let(:request_body) { { :event_id => event_id } }
587
+ let(:correct_response_code) { 202 }
588
+ let(:correct_response_body) { nil }
589
+
590
+ subject { client.delete_event(calendar_id, event_id) }
591
+
592
+ it_behaves_like 'a Cronofy request'
593
+ end
594
+
595
+ describe '#delete_external_event' do
596
+ let(:calendar_id) { 'calendar_id_123'}
597
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events" }
598
+ let(:event_uid) { 'external_event_1023' }
599
+ let(:method) { :delete }
600
+ let(:request_headers) { json_request_headers }
601
+ let(:request_body) { { :event_uid => event_uid } }
602
+ let(:correct_response_code) { 202 }
603
+ let(:correct_response_body) { nil }
604
+
605
+ subject { client.delete_external_event(calendar_id, event_uid) }
606
+
607
+ it_behaves_like 'a Cronofy request'
608
+ end
609
+
610
+ describe '#change_participation_status' do
611
+ let(:calendar_id) { 'calendar_id_123'}
612
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events/#{event_uid}/participation_status" }
613
+ let(:event_uid) { 'evt_external_54008b1a4a41730f8d5c6037' }
614
+ let(:method) { :post }
615
+ let(:request_headers) { json_request_headers }
616
+ let(:status) { 'accepted' }
617
+ let(:request_body) { { :status => status } }
618
+ let(:correct_response_code) { 202 }
619
+ let(:correct_response_body) { nil }
620
+
621
+ subject { client.change_participation_status(calendar_id, event_uid, status) }
622
+
623
+ it_behaves_like 'a Cronofy request'
624
+ end
625
+
626
+ describe '#delete_all_events' do
627
+ context "default" do
628
+ let(:request_url) { "https://api.cronofy.com/v1/events" }
629
+ let(:method) { :delete }
630
+ let(:request_headers) { json_request_headers }
631
+ let(:request_body) { { :delete_all => true } }
632
+ let(:correct_response_code) { 202 }
633
+ let(:correct_response_body) { nil }
634
+
635
+ subject { client.delete_all_events }
636
+
637
+ it_behaves_like 'a Cronofy request'
638
+ end
639
+
640
+ context "specific calendars" do
641
+ let(:calendar_ids) { %w{cal_1234_5678 cal_abcd_efgh} }
642
+ let(:request_url) { "https://api.cronofy.com/v1/events" }
643
+ let(:method) { :delete }
644
+ let(:request_headers) { json_request_headers }
645
+ let(:request_body) { { :calendar_ids => calendar_ids } }
646
+ let(:correct_response_code) { 202 }
647
+ let(:correct_response_body) { nil }
648
+
649
+ subject { client.delete_all_events(calendar_ids: calendar_ids) }
650
+
651
+ it_behaves_like 'a Cronofy request'
652
+ end
653
+ end
654
+ end
655
+
656
+ describe 'Service Account impersonation' do
657
+ let(:calendar_id) { 'calendar_id_123'}
658
+ let(:request_url) { "https://api.cronofy.com/v1/service_account_authorizations" }
659
+ let(:method) { :post }
660
+ let(:request_headers) { json_request_headers }
661
+ let(:request_body) { { email: email, scope: scope.join(' '), callback_url: callback_url } }
662
+ let(:correct_response_code) { 202 }
663
+ let(:correct_response_body) { nil }
664
+ let(:email) { "foo@example.com" }
665
+ let(:scope) { ['foo', 'bar'] }
666
+ let(:callback_url) { "http://example.com/not_found" }
667
+
668
+ subject { client.authorize_with_service_account(email, scope, callback_url) }
669
+
670
+ it_behaves_like 'a Cronofy request'
671
+ end
672
+
673
+ describe 'Channels' do
674
+ let(:request_url) { 'https://api.cronofy.com/v1/channels' }
675
+
676
+ describe '#create_channel' do
677
+ let(:method) { :post }
678
+ let(:callback_url) { 'http://call.back/url' }
679
+ let(:request_headers) { json_request_headers }
680
+
681
+ let(:correct_response_code) { 200 }
682
+ let(:correct_response_body) do
683
+ {
684
+ 'channel' => {
685
+ 'channel_id' => 'channel_id_123',
686
+ 'callback_url' => ENV['CALLBACK_URL'],
687
+ 'filters' => {}
688
+ }
689
+ }
690
+ end
691
+
692
+ let(:correct_mapped_result) do
693
+ Cronofy::Channel.new(correct_response_body["channel"])
694
+ end
695
+
696
+ context "with filters" do
697
+ let(:request_body) do
698
+ {
699
+ callback_url: callback_url,
700
+ filters: filters,
701
+ }
702
+ end
703
+
704
+ let(:filters) do
705
+ {
706
+ calendar_ids: ["cal_1234_abcd", "cal_1234_efgh", "cal_5678_ijkl"],
707
+ only_managed: true,
708
+ future_parameter: "for flexibility",
709
+ }
710
+ end
711
+
712
+ subject { client.create_channel(callback_url, filters: filters) }
713
+
714
+ it_behaves_like 'a Cronofy request'
715
+ it_behaves_like 'a Cronofy request with mapped return value'
716
+ end
717
+
718
+ context "without filters" do
719
+ let(:request_body) do
720
+ {
721
+ callback_url: callback_url,
722
+ }
723
+ end
724
+
725
+ subject { client.create_channel(callback_url) }
726
+
727
+ it_behaves_like 'a Cronofy request'
728
+ it_behaves_like 'a Cronofy request with mapped return value'
729
+ end
730
+ end
731
+
732
+ describe '#list_channels' do
733
+ let(:method) { :get }
734
+
735
+ let(:correct_response_code) { 200 }
736
+ let(:correct_response_body) do
737
+ {
738
+ 'channels' => [
739
+ {
740
+ 'channel_id' => 'channel_id_123',
741
+ 'callback_url' => 'http://call.back/url',
742
+ 'filters' => {}
743
+ },
744
+ {
745
+ 'channel_id' => 'channel_id_456',
746
+ 'callback_url' => 'http://call.back/url2',
747
+ 'filters' => {}
748
+ }
749
+ ]
750
+ }
751
+ end
752
+
753
+ let(:correct_mapped_result) do
754
+ correct_response_body["channels"].map { |ch| Cronofy::Channel.new(ch) }
755
+ end
756
+
757
+ subject { client.list_channels }
758
+
759
+ it_behaves_like 'a Cronofy request'
760
+ it_behaves_like 'a Cronofy request with mapped return value'
761
+ end
762
+
763
+ describe '#close_channel' do
764
+ let(:channel_id) { "chn_1234567890" }
765
+ let(:method) { :delete }
766
+ let(:request_url) { "https://api.cronofy.com/v1/channels/#{channel_id}" }
767
+
768
+ let(:correct_response_code) { 202 }
769
+ let(:correct_response_body) { nil }
770
+
771
+ subject { client.close_channel(channel_id) }
772
+
773
+ it_behaves_like 'a Cronofy request'
774
+ end
775
+ end
776
+
777
+ describe "ElevatedPermissions" do
778
+
779
+ describe '#elevated_permissions' do
780
+ let(:method) { :post }
781
+ let(:request_url) { "https://api.cronofy.com/v1/permissions" }
782
+
783
+ let(:correct_response_code) { 202 }
784
+
785
+ let(:redirect_uri) { "http://www.example.com/redirect" }
786
+ let(:permissions) do
787
+ [
788
+ {
789
+ calendar_id: "cal_1234567",
790
+ permission_level: "unrestricted"
791
+ },
792
+ {
793
+ calendar_id: "cal_1234453",
794
+ permission_level: "sandbox"
795
+ }
796
+ ]
797
+ end
798
+
799
+ let(:request_body) do
800
+ {
801
+ permissions: permissions,
802
+ redirect_uri: redirect_uri,
803
+ }
804
+ end
805
+
806
+ let(:correct_mapped_result) do
807
+ Cronofy::PermissionsResponse.new(correct_response_body['permissions_request'])
808
+ end
809
+
810
+ describe "with uri supplied" do
811
+ let(:correct_response_body) do
812
+ {
813
+ "permissions_request" => {
814
+ "url" => "http://app.cronofy.com/permissions/"
815
+ }
816
+ }
817
+ end
818
+
819
+ subject { client.elevated_permissions(permissions: permissions, redirect_uri: redirect_uri) }
820
+
821
+ it_behaves_like 'a Cronofy request'
822
+ it_behaves_like 'a Cronofy request with mapped return value'
823
+ end
824
+
825
+ describe "without uri supplied" do
826
+ let(:correct_response_body) do
827
+ {
828
+ "permissions_request" => {
829
+ "accepted" => true
830
+ }
831
+ }
832
+ end
833
+
834
+ subject { client.elevated_permissions(permissions: permissions, redirect_uri: redirect_uri) }
835
+
836
+ it_behaves_like 'a Cronofy request'
837
+ it_behaves_like 'a Cronofy request with mapped return value'
838
+ end
839
+ end
840
+ end
841
+
842
+ describe "Account" do
843
+ let(:request_url) { "https://api.cronofy.com/v1/account" }
844
+
845
+ describe "#account" do
846
+ let(:method) { :get }
847
+
848
+ let(:correct_response_code) { 200 }
849
+ let(:correct_response_body) do
850
+ {
851
+ "account" => {
852
+ "account_id" => "acc_id_123",
853
+ "email" => "foo@example.com",
854
+ }
855
+ }
856
+ end
857
+
858
+ let(:correct_mapped_result) do
859
+ Cronofy::Account.new(correct_response_body["account"])
860
+ end
861
+
862
+ subject { client.account }
863
+
864
+ it_behaves_like "a Cronofy request"
865
+ it_behaves_like "a Cronofy request with mapped return value"
866
+ end
867
+ end
868
+
869
+ describe "Userinfo" do
870
+ let(:request_url) { "https://api.cronofy.com/v1/userinfo" }
871
+
872
+ describe "#userinfo" do
873
+ let(:method) { :get }
874
+
875
+ let(:correct_response_code) { 200 }
876
+ let(:correct_response_body) do
877
+ {
878
+ "sub" => "ser_5700a00eb0ccd07000000000",
879
+ "cronofy.type" => "service_account",
880
+ "cronofy.service_account.domain" => "example.com"
881
+ }
882
+ end
883
+
884
+ let(:correct_mapped_result) do
885
+ Cronofy::UserInfo.new(correct_response_body)
886
+ end
887
+
888
+ subject { client.userinfo }
889
+
890
+ it_behaves_like "a Cronofy request"
891
+ it_behaves_like "a Cronofy request with mapped return value"
892
+ end
893
+ end
894
+
895
+ describe 'Profiles' do
896
+ let(:request_url) { 'https://api.cronofy.com/v1/profiles' }
897
+
898
+ describe '#profiles' do
899
+ let(:method) { :get }
900
+
901
+ let(:correct_response_code) { 200 }
902
+ let(:correct_response_body) do
903
+ {
904
+ 'profiles' => [
905
+ {
906
+ 'provider_name' => 'google',
907
+ 'profile_id' => 'pro_n23kjnwrw2',
908
+ 'profile_name' => 'example@cronofy.com',
909
+ 'profile_connected' => true,
910
+ },
911
+ {
912
+ 'provider_name' => 'apple',
913
+ 'profile_id' => 'pro_n23kjnwrw2',
914
+ 'profile_name' => 'example@cronofy.com',
915
+ 'profile_connected' => false,
916
+ 'profile_relink_url' => 'http =>//to.cronofy.com/RaNggYu',
917
+ },
918
+ ]
919
+ }
920
+ end
921
+
922
+ let(:correct_mapped_result) do
923
+ correct_response_body["profiles"].map { |pro| Cronofy::Profile.new(pro) }
924
+ end
925
+
926
+ subject { client.list_profiles }
927
+
928
+ it_behaves_like 'a Cronofy request'
929
+ it_behaves_like 'a Cronofy request with mapped return value'
930
+ end
931
+ end
932
+
933
+ describe 'Free busy' do
934
+ describe '#free_busy' do
935
+ before do
936
+ stub_request(method, request_url)
937
+ .with(headers: request_headers,
938
+ body: request_body)
939
+ .to_return(status: correct_response_code,
940
+ headers: correct_response_headers,
941
+ body: correct_response_body.to_json)
942
+
943
+ stub_request(:get, next_page_url)
944
+ .with(headers: request_headers)
945
+ .to_return(status: correct_response_code,
946
+ headers: correct_response_headers,
947
+ body: next_page_body.to_json)
948
+ end
949
+
950
+
951
+ let(:request_url_prefix) { 'https://api.cronofy.com/v1/free_busy' }
952
+ let(:method) { :get }
953
+ let(:correct_response_code) { 200 }
954
+ let(:next_page_url) do
955
+ "https://next.page.com/08a07b034306679e"
956
+ end
957
+
958
+ let(:params) { Hash.new }
959
+ let(:request_url) { request_url_prefix + "?tzid=Etc/UTC" }
960
+
961
+ let(:correct_response_body) do
962
+ {
963
+ 'pages' => {
964
+ 'current' => 1,
965
+ 'total' => 2,
966
+ 'next_page' => next_page_url
967
+ },
968
+ 'free_busy' => [
969
+ {
970
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
971
+ 'start' => '2014-09-06',
972
+ 'end' => '2014-09-08',
973
+ 'free_busy_status' => 'busy',
974
+ },
975
+ {
976
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
977
+ 'start' => '2014-09-13T19:00:00Z',
978
+ 'end' => '2014-09-13T21:00:00Z',
979
+ 'free_busy_status' => 'tentative',
980
+ }
981
+ ]
982
+ }
983
+ end
984
+
985
+ let(:next_page_body) do
986
+ {
987
+ 'pages' => {
988
+ 'current' => 2,
989
+ 'total' => 2,
990
+ },
991
+ 'free_busy' => [
992
+ {
993
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
994
+ 'start' => '2014-09-07',
995
+ 'end' => '2014-09-09',
996
+ 'free_busy_status' => 'busy',
997
+ },
998
+ {
999
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
1000
+ 'start' => '2014-09-14T19:00:00Z',
1001
+ 'end' => '2014-09-14T21:00:00Z',
1002
+ 'free_busy_status' => 'tentative',
1003
+ }
1004
+ ]
1005
+ }
1006
+ end
1007
+
1008
+ let(:correct_mapped_result) do
1009
+ first_page_items = correct_response_body['free_busy'].map { |period| Cronofy::FreeBusy.new(period) }
1010
+ second_page_items = next_page_body['free_busy'].map { |period| Cronofy::FreeBusy.new(period) }
1011
+
1012
+ first_page_items + second_page_items
1013
+ end
1014
+
1015
+ subject do
1016
+ # By default force evaluation
1017
+ client.free_busy(params).to_a
1018
+ end
1019
+
1020
+ context 'when all params are passed' do
1021
+ let(:params) do
1022
+ {
1023
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
1024
+ to: Time.new(2014, 10, 1, 0, 0, 1, '+00:00'),
1025
+ tzid: 'Etc/UTC',
1026
+ include_managed: true,
1027
+ }
1028
+ end
1029
+ let(:request_url) do
1030
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
1031
+ "&to=2014-10-01T00:00:01Z&tzid=Etc/UTC&include_managed=true"
1032
+ end
1033
+
1034
+ it_behaves_like 'a Cronofy request'
1035
+ it_behaves_like 'a Cronofy request with mapped return value'
1036
+ end
1037
+
1038
+ context 'when some params are passed' do
1039
+ let(:params) do
1040
+ {
1041
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
1042
+ }
1043
+ end
1044
+ let(:request_url) do
1045
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
1046
+ "&tzid=Etc/UTC"
1047
+ end
1048
+
1049
+ it_behaves_like 'a Cronofy request'
1050
+ it_behaves_like 'a Cronofy request with mapped return value'
1051
+ end
1052
+
1053
+ context "when unknown flags are passed" do
1054
+ let(:params) do
1055
+ {
1056
+ unknown_bool: true,
1057
+ unknown_number: 5,
1058
+ unknown_string: "foo-bar-baz",
1059
+ }
1060
+ end
1061
+
1062
+ let(:request_url) do
1063
+ "#{request_url_prefix}?tzid=Etc/UTC" \
1064
+ "&unknown_bool=true" \
1065
+ "&unknown_number=5" \
1066
+ "&unknown_string=foo-bar-baz"
1067
+ end
1068
+
1069
+ it_behaves_like 'a Cronofy request'
1070
+ it_behaves_like 'a Cronofy request with mapped return value'
1071
+ end
1072
+
1073
+ context "next page not found" do
1074
+ before do
1075
+ stub_request(:get, next_page_url)
1076
+ .with(headers: request_headers)
1077
+ .to_return(status: 404,
1078
+ headers: correct_response_headers)
1079
+ end
1080
+
1081
+ it "raises an error" do
1082
+ expect{ subject }.to raise_error(::Cronofy::NotFoundError)
1083
+ end
1084
+ end
1085
+
1086
+ context "only first period" do
1087
+ before do
1088
+ # Ensure an error if second page is requested
1089
+ stub_request(:get, next_page_url)
1090
+ .with(headers: request_headers)
1091
+ .to_return(status: 404,
1092
+ headers: correct_response_headers)
1093
+ end
1094
+
1095
+ let(:first_period) do
1096
+ Cronofy::FreeBusy.new(correct_response_body["free_busy"].first)
1097
+ end
1098
+
1099
+ subject do
1100
+ client.free_busy(params).first
1101
+ end
1102
+
1103
+ it "returns the first period from the first page" do
1104
+ expect(subject).to eq(first_period)
1105
+ end
1106
+ end
1107
+
1108
+ context "without calling #to_a to force full evaluation" do
1109
+ subject { client.free_busy(params) }
1110
+
1111
+ it_behaves_like 'a Cronofy request'
1112
+
1113
+ # We expect it to behave like a Cronofy request as the first page is
1114
+ # requested eagerly so that the majority of errors will happen inline
1115
+ # rather than lazily happening wherever the iterator may have been
1116
+ # passed.
1117
+ end
1118
+ end
1119
+ end
1120
+
1121
+ describe 'Resources' do
1122
+ let(:request_url) { 'https://api.cronofy.com/v1/resources' }
1123
+
1124
+ describe '#resources' do
1125
+ let(:method) { :get }
1126
+
1127
+ let(:correct_response_code) { 200 }
1128
+ let(:correct_response_body) do
1129
+ {
1130
+ 'resources' => [
1131
+ {
1132
+ 'email' => 'board-room-london@example.com',
1133
+ 'name' => 'Board room (London)',
1134
+ },
1135
+ {
1136
+ 'email' => 'board-room-madrid@example.com',
1137
+ 'name' => 'Board room (Madrid)',
1138
+ }
1139
+ ]
1140
+ }
1141
+ end
1142
+
1143
+ let(:correct_mapped_result) do
1144
+ correct_response_body["resources"].map { |r| Cronofy::Resource.new(r) }
1145
+ end
1146
+
1147
+ subject { client.resources }
1148
+
1149
+ it_behaves_like 'a Cronofy request'
1150
+ it_behaves_like 'a Cronofy request with mapped return value'
1151
+ end
1152
+ end
1153
+
1154
+ describe '#link_token' do
1155
+ let(:request_url) { 'https://api.cronofy.com/v1/link_tokens' }
1156
+ let(:method) { :post }
1157
+ let(:request_body) { nil }
1158
+
1159
+ let(:correct_response_code) { 200 }
1160
+ let(:correct_response_body) do
1161
+ {
1162
+ "link_token" => "abcd1234"
1163
+ }
1164
+ end
1165
+
1166
+ let(:correct_mapped_result) do
1167
+ "abcd1234"
1168
+ end
1169
+
1170
+ subject { client.link_token }
1171
+
1172
+ it_behaves_like 'a Cronofy request'
1173
+ it_behaves_like 'a Cronofy request with mapped return value'
1174
+ end
1175
+
1176
+ describe '#element_token' do
1177
+ let(:permissions) { ["agenda", "availability"] }
1178
+ let(:subs) { ["acc_567236000909002", "acc_678347111010113"] }
1179
+ let(:origin) { "https://local.test/page" }
1180
+
1181
+ let(:request_url) { 'https://api.cronofy.com/v1/element_tokens' }
1182
+ let(:method) { :post }
1183
+
1184
+ let(:request_headers) do
1185
+ {
1186
+ "Authorization" => "Bearer #{client_secret}",
1187
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
1188
+ "Content-Type" => "application/json; charset=utf-8",
1189
+ }
1190
+ end
1191
+
1192
+ let(:request_body) do
1193
+ {
1194
+ permissions: permissions,
1195
+ subs: subs,
1196
+ origin: origin
1197
+ }
1198
+ end
1199
+
1200
+ let(:expected_token) { "ELEMENT_TOKEN_1276534" }
1201
+
1202
+ let(:correct_response_code) { 200 }
1203
+ let(:correct_response_body) do
1204
+ {
1205
+ "element_token" => {
1206
+ "permissions" => permissions,
1207
+ "origin" => origin,
1208
+ "token" => expected_token,
1209
+ "expires_in" => 64800
1210
+ }
1211
+ }
1212
+ end
1213
+
1214
+ let(:correct_mapped_result) do
1215
+ Cronofy::ElementToken.new(correct_response_body['element_token'])
1216
+ end
1217
+
1218
+ let(:client_id) { 'example_id' }
1219
+ let(:client_secret) { 'example_secret' }
1220
+
1221
+ let(:client) do
1222
+ Cronofy::Client.new(
1223
+ client_id: client_id,
1224
+ client_secret: client_secret,
1225
+ )
1226
+ end
1227
+
1228
+ subject do
1229
+ client.element_token({
1230
+ permissions: permissions,
1231
+ subs: subs,
1232
+ origin: origin
1233
+ })
1234
+ end
1235
+
1236
+ it_behaves_like 'a Cronofy request'
1237
+ it_behaves_like 'a Cronofy request with mapped return value'
1238
+ end
1239
+
1240
+ describe '#revoke_profile_authorization' do
1241
+ let(:request_url) { "https://api.cronofy.com/v1/profiles/#{profile_id}/revoke" }
1242
+ let(:method) { :post }
1243
+ let(:request_body) { nil }
1244
+ let(:profile_id) { "pro_1234abc" }
1245
+
1246
+ let(:correct_response_code) { 202 }
1247
+ let(:correct_response_body) { nil }
1248
+
1249
+ subject { client.revoke_profile_authorization(profile_id) }
1250
+
1251
+ it_behaves_like 'a Cronofy request'
1252
+ end
1253
+
1254
+ describe 'Availability' do
1255
+ describe '#availability' do
1256
+ let(:method) { :post }
1257
+ let(:request_url) { 'https://api.cronofy.com/v1/availability' }
1258
+ let(:request_headers) { json_request_headers }
1259
+
1260
+ let(:client_id) { 'example_id' }
1261
+ let(:client_secret) { 'example_secret' }
1262
+ let(:token) { client_secret }
1263
+
1264
+ let(:client) do
1265
+ Cronofy::Client.new(
1266
+ client_id: client_id,
1267
+ client_secret: client_secret,
1268
+ )
1269
+ end
1270
+
1271
+ let(:request_body) do
1272
+ {
1273
+ "participants" => [
1274
+ {
1275
+ "members" => [
1276
+ { "sub" => "acc_567236000909002" },
1277
+ { "sub" => "acc_678347111010113" }
1278
+ ],
1279
+ "required" => "all"
1280
+ }
1281
+ ],
1282
+ "required_duration" => { "minutes" => 60 },
1283
+ "available_periods" => [
1284
+ {
1285
+ "start" => "2017-01-03T09:00:00Z",
1286
+ "end" => "2017-01-03T18:00:00Z"
1287
+ },
1288
+ {
1289
+ "start" => "2017-01-04T09:00:00Z",
1290
+ "end" => "2017-01-04T18:00:00Z"
1291
+ }
1292
+ ]
1293
+ }
1294
+ end
1295
+
1296
+ let(:availability_response_attribute) { "available_periods" }
1297
+ let(:availability_response_class) { Cronofy::AvailablePeriod }
1298
+
1299
+ let(:correct_response_code) { 200 }
1300
+ let(:correct_response_body) do
1301
+ {
1302
+ availability_response_attribute => [
1303
+ {
1304
+ "start" => "2017-01-03T09:00:00Z",
1305
+ "end" => "2017-01-03T11:00:00Z",
1306
+ "participants" => [
1307
+ { "sub" => "acc_567236000909002" },
1308
+ { "sub" => "acc_678347111010113" }
1309
+ ]
1310
+ },
1311
+ {
1312
+ "start" => "2017-01-03T14 =>00:00Z",
1313
+ "end" => "2017-01-03T16:00:00Z",
1314
+ "participants" => [
1315
+ { "sub" => "acc_567236000909002" },
1316
+ { "sub" => "acc_678347111010113" }
1317
+ ]
1318
+ },
1319
+ {
1320
+ "start" => "2017-01-04T11:00:00Z",
1321
+ "end" => "2017-01-04T17:00:00Z",
1322
+ "participants" => [
1323
+ { "sub" => "acc_567236000909002" },
1324
+ { "sub" => "acc_678347111010113" }
1325
+ ]
1326
+ },
1327
+ ]
1328
+ }
1329
+ end
1330
+
1331
+ let(:correct_mapped_result) do
1332
+ correct_response_body[availability_response_attribute].map { |ap| availability_response_class.new(ap) }
1333
+ end
1334
+
1335
+ subject { client.availability(participants: participants, required_duration: required_duration, available_periods: available_periods) }
1336
+
1337
+ context "response_format" do
1338
+ %i{
1339
+ slots
1340
+ overlapping_slots
1341
+ }.each do |slot_format|
1342
+ context slot_format do
1343
+ let(:response_format) { slot_format.to_s }
1344
+ let(:availability_response_attribute) { "available_slots" }
1345
+ let(:availability_response_class) { Cronofy::AvailableSlot }
1346
+
1347
+ let(:required_duration) do
1348
+ { minutes: 60 }
1349
+ end
1350
+
1351
+ let(:available_periods) do
1352
+ [
1353
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1354
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1355
+ ]
1356
+ end
1357
+
1358
+ let(:participants) do
1359
+ [
1360
+ {
1361
+ members: [
1362
+ { sub: "acc_567236000909002" },
1363
+ { sub: "acc_678347111010113" },
1364
+ ],
1365
+ required: :all,
1366
+ }
1367
+ ]
1368
+ end
1369
+
1370
+ let(:request_body) do
1371
+ {
1372
+ "participants" => [
1373
+ {
1374
+ "members" => [
1375
+ { "sub" => "acc_567236000909002" },
1376
+ { "sub" => "acc_678347111010113" }
1377
+ ],
1378
+ "required" => "all"
1379
+ }
1380
+ ],
1381
+ "required_duration" => { "minutes" => 60 },
1382
+ "available_periods" => [
1383
+ {
1384
+ "start" => "2017-01-03T09:00:00Z",
1385
+ "end" => "2017-01-03T18:00:00Z"
1386
+ },
1387
+ {
1388
+ "start" => "2017-01-04T09:00:00Z",
1389
+ "end" => "2017-01-04T18:00:00Z"
1390
+ }
1391
+ ],
1392
+ "response_format" => response_format,
1393
+ }
1394
+ end
1395
+
1396
+ subject do
1397
+ client.availability(
1398
+ participants: participants,
1399
+ required_duration: required_duration,
1400
+ available_periods: available_periods,
1401
+ response_format: slot_format,
1402
+ )
1403
+ end
1404
+
1405
+ it_behaves_like 'a Cronofy request'
1406
+ it_behaves_like 'a Cronofy request with mapped return value'
1407
+ end
1408
+ end
1409
+ end
1410
+
1411
+ context "fully specified" do
1412
+ let(:participants) do
1413
+ [
1414
+ {
1415
+ members: [
1416
+ { sub: "acc_567236000909002" },
1417
+ { sub: "acc_678347111010113" },
1418
+ ],
1419
+ required: :all,
1420
+ }
1421
+ ]
1422
+ end
1423
+
1424
+ let(:required_duration) do
1425
+ { minutes: 60 }
1426
+ end
1427
+
1428
+ let(:available_periods) do
1429
+ [
1430
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1431
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1432
+ ]
1433
+ end
1434
+
1435
+ it_behaves_like 'a Cronofy request'
1436
+ it_behaves_like 'a Cronofy request with mapped return value'
1437
+ end
1438
+
1439
+ context "buffer and start_interval" do
1440
+ let(:request_body) do
1441
+ {
1442
+ "participants" => [
1443
+ {
1444
+ "members" => [
1445
+ { "sub" => "acc_567236000909002" },
1446
+ { "sub" => "acc_678347111010113" }
1447
+ ],
1448
+ "required" => "all"
1449
+ }
1450
+ ],
1451
+ "required_duration" => { "minutes" => 60 },
1452
+ "buffer" => {
1453
+ "before": { "minutes": 30 },
1454
+ "after": { "minutes": 60 },
1455
+ },
1456
+ "start_interval" => { "minutes" => 60 },
1457
+ "available_periods" => [
1458
+ {
1459
+ "start" => "2017-01-03T09:00:00Z",
1460
+ "end" => "2017-01-03T18:00:00Z"
1461
+ },
1462
+ {
1463
+ "start" => "2017-01-04T09:00:00Z",
1464
+ "end" => "2017-01-04T18:00:00Z"
1465
+ },
1466
+ ]
1467
+ }
1468
+ end
1469
+
1470
+ let(:participants) do
1471
+ [
1472
+ {
1473
+ members: [
1474
+ { sub: "acc_567236000909002" },
1475
+ { sub: "acc_678347111010113" },
1476
+ ],
1477
+ required: :all,
1478
+ }
1479
+ ]
1480
+ end
1481
+
1482
+ let(:buffer) do
1483
+ {
1484
+ before: { minutes: 30 },
1485
+ after: { minutes: 60 },
1486
+ }
1487
+ end
1488
+
1489
+ let(:start_interval) do
1490
+ { minutes: 60 }
1491
+ end
1492
+
1493
+ let(:required_duration) do
1494
+ { minutes: 60 }
1495
+ end
1496
+
1497
+ let(:available_periods) do
1498
+ [
1499
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1500
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1501
+ ]
1502
+ end
1503
+
1504
+ subject { client.availability(participants: participants, required_duration: required_duration, available_periods: available_periods, buffer: buffer, start_interval: start_interval) }
1505
+
1506
+ it_behaves_like 'a Cronofy request'
1507
+ it_behaves_like 'a Cronofy request with mapped return value'
1508
+ end
1509
+
1510
+ context "member-specific available periods" do
1511
+ let(:request_body) do
1512
+ {
1513
+ "participants" => [
1514
+ {
1515
+ "members" => [
1516
+ { "sub" => "acc_567236000909002" },
1517
+ {
1518
+ "sub" => "acc_678347111010113",
1519
+ "available_periods" => [
1520
+ {
1521
+ "start" => "2017-01-03T09:00:00Z",
1522
+ "end" => "2017-01-03T12:00:00Z"
1523
+ },
1524
+ {
1525
+ "start" => "2017-01-04T10:00:00Z",
1526
+ "end" => "2017-01-04T20:00:00Z"
1527
+ }
1528
+ ]
1529
+ }
1530
+ ],
1531
+ "required" => "all"
1532
+ }
1533
+ ],
1534
+ "required_duration" => { "minutes" => 60 },
1535
+ "available_periods" => [
1536
+ {
1537
+ "start" => "2017-01-03T09:00:00Z",
1538
+ "end" => "2017-01-03T18:00:00Z"
1539
+ },
1540
+ {
1541
+ "start" => "2017-01-04T09:00:00Z",
1542
+ "end" => "2017-01-04T18:00:00Z"
1543
+ }
1544
+ ]
1545
+ }
1546
+ end
1547
+
1548
+ let(:participants) do
1549
+ [
1550
+ {
1551
+ members: [
1552
+ { sub: "acc_567236000909002" },
1553
+ {
1554
+ sub: "acc_678347111010113",
1555
+ available_periods: [
1556
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T12:00:00Z") },
1557
+ { start: Time.parse("2017-01-04T10:00:00Z"), end: Time.parse("2017-01-04T20:00:00Z") },
1558
+ ],
1559
+ },
1560
+ ],
1561
+ required: :all,
1562
+ }
1563
+ ]
1564
+ end
1565
+
1566
+ let(:required_duration) do
1567
+ { minutes: 60 }
1568
+ end
1569
+
1570
+ let(:available_periods) do
1571
+ [
1572
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1573
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1574
+ ]
1575
+ end
1576
+
1577
+ it_behaves_like 'a Cronofy request'
1578
+ it_behaves_like 'a Cronofy request with mapped return value'
1579
+ end
1580
+
1581
+ context "reused member-specific available periods" do
1582
+ let(:request_body) do
1583
+ {
1584
+ "participants" => [
1585
+ {
1586
+ "members" => [
1587
+ {
1588
+ "sub" => "acc_567236000909002",
1589
+ "available_periods" => [
1590
+ {
1591
+ "start" => "2017-01-03T09:00:00Z",
1592
+ "end" => "2017-01-03T12:00:00Z"
1593
+ },
1594
+ ]
1595
+ },
1596
+ {
1597
+ "sub" => "acc_678347111010113",
1598
+ "available_periods" => [
1599
+ {
1600
+ "start" => "2017-01-03T09:00:00Z",
1601
+ "end" => "2017-01-03T12:00:00Z"
1602
+ },
1603
+ ]
1604
+ }
1605
+ ],
1606
+ "required" => "all"
1607
+ }
1608
+ ],
1609
+ "required_duration" => { "minutes" => 60 },
1610
+ "available_periods" => [
1611
+ {
1612
+ "start" => "2017-01-03T09:00:00Z",
1613
+ "end" => "2017-01-03T18:00:00Z"
1614
+ },
1615
+ {
1616
+ "start" => "2017-01-04T09:00:00Z",
1617
+ "end" => "2017-01-04T18:00:00Z"
1618
+ }
1619
+ ]
1620
+ }
1621
+ end
1622
+
1623
+ let(:participants) do
1624
+ available_periods = [{ start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T12:00:00Z") }]
1625
+ [
1626
+ {
1627
+ members: [
1628
+ { sub: "acc_567236000909002", available_periods: available_periods },
1629
+ { sub: "acc_678347111010113", available_periods: available_periods },
1630
+ ],
1631
+ required: :all,
1632
+ }
1633
+ ]
1634
+ end
1635
+
1636
+ let(:required_duration) do
1637
+ { minutes: 60 }
1638
+ end
1639
+
1640
+ let(:available_periods) do
1641
+ [
1642
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1643
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1644
+ ]
1645
+ end
1646
+
1647
+ it_behaves_like 'a Cronofy request'
1648
+ it_behaves_like 'a Cronofy request with mapped return value'
1649
+ end
1650
+
1651
+ context "member-specific calendars" do
1652
+ let(:request_body) do
1653
+ {
1654
+ "participants" => [
1655
+ {
1656
+ "members" => [
1657
+ { "sub" => "acc_567236000909002" },
1658
+ {
1659
+ "sub" => "acc_678347111010113",
1660
+ "calendar_ids" => [
1661
+ "cal_1234_5678",
1662
+ "cal_9876_5432",
1663
+ ]
1664
+ }
1665
+ ],
1666
+ "required" => "all"
1667
+ }
1668
+ ],
1669
+ "required_duration" => { "minutes" => 60 },
1670
+ "available_periods" => [
1671
+ {
1672
+ "start" => "2017-01-03T09:00:00Z",
1673
+ "end" => "2017-01-03T18:00:00Z"
1674
+ },
1675
+ {
1676
+ "start" => "2017-01-04T09:00:00Z",
1677
+ "end" => "2017-01-04T18:00:00Z"
1678
+ }
1679
+ ]
1680
+ }
1681
+ end
1682
+
1683
+ let(:participants) do
1684
+ [
1685
+ {
1686
+ members: [
1687
+ { sub: "acc_567236000909002" },
1688
+ {
1689
+ sub: "acc_678347111010113",
1690
+ calendar_ids: [
1691
+ "cal_1234_5678",
1692
+ "cal_9876_5432",
1693
+ ],
1694
+ },
1695
+ ],
1696
+ required: :all,
1697
+ }
1698
+ ]
1699
+ end
1700
+
1701
+ let(:required_duration) do
1702
+ { minutes: 60 }
1703
+ end
1704
+
1705
+ let(:available_periods) do
1706
+ [
1707
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1708
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1709
+ ]
1710
+ end
1711
+
1712
+ it_behaves_like 'a Cronofy request'
1713
+ it_behaves_like 'a Cronofy request with mapped return value'
1714
+ end
1715
+
1716
+ context "simple values to defaults" do
1717
+ let(:participants) do
1718
+ { members: %w{acc_567236000909002 acc_678347111010113} }
1719
+ end
1720
+
1721
+ let(:required_duration) { 60 }
1722
+
1723
+ let(:available_periods) do
1724
+ [
1725
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1726
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1727
+ ]
1728
+ end
1729
+
1730
+ it_behaves_like 'a Cronofy request'
1731
+ it_behaves_like 'a Cronofy request with mapped return value'
1732
+ end
1733
+
1734
+ context "when given query_periods instead of available_periods" do
1735
+ let(:participants) do
1736
+ { members: %w{acc_567236000909002 acc_678347111010113} }
1737
+ end
1738
+
1739
+ let(:required_duration) { 60 }
1740
+
1741
+ let(:query_periods) do
1742
+ [
1743
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1744
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1745
+ ]
1746
+ end
1747
+
1748
+ let(:request_body) do
1749
+ {
1750
+ "participants" => [
1751
+ {
1752
+ "members" => [
1753
+ { "sub" => "acc_567236000909002" },
1754
+ { "sub" => "acc_678347111010113" }
1755
+ ],
1756
+ "required" => "all"
1757
+ }
1758
+ ],
1759
+ "query_periods" => [
1760
+ {
1761
+ "start" => "2017-01-03T09:00:00Z",
1762
+ "end" => "2017-01-03T18:00:00Z"
1763
+ },
1764
+ {
1765
+ "start" => "2017-01-04T09:00:00Z",
1766
+ "end" => "2017-01-04T18:00:00Z"
1767
+ }
1768
+ ],
1769
+ "required_duration" => { "minutes" => 60 },
1770
+ }
1771
+ end
1772
+
1773
+ subject do
1774
+ client.availability(
1775
+ participants: participants,
1776
+ required_duration: required_duration,
1777
+ query_periods: query_periods
1778
+ )
1779
+ end
1780
+
1781
+ it_behaves_like 'a Cronofy request'
1782
+ it_behaves_like 'a Cronofy request with mapped return value'
1783
+ end
1784
+
1785
+ context "when trying to auth with only an access_token, as originally implemented" do
1786
+ let(:access_token) { "access_token_123"}
1787
+ let(:client) { Cronofy::Client.new(access_token: access_token) }
1788
+ let(:request_headers) do
1789
+ {
1790
+ "Authorization" => "Bearer #{access_token}",
1791
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
1792
+ "Content-Type" => "application/json; charset=utf-8",
1793
+ }
1794
+ end
1795
+
1796
+ let(:participants) do
1797
+ { members: %w{acc_567236000909002 acc_678347111010113} }
1798
+ end
1799
+
1800
+ let(:required_duration) { 60 }
1801
+
1802
+ let(:available_periods) do
1803
+ [
1804
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1805
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1806
+ ]
1807
+ end
1808
+
1809
+ it_behaves_like 'a Cronofy request'
1810
+ it_behaves_like 'a Cronofy request with mapped return value'
1811
+ end
1812
+
1813
+ context "when trying to auth with both a client_secret and access_token" do
1814
+ let(:access_token) { "access_token_123" }
1815
+ let(:client_secret) { "client_secret_456" }
1816
+ let(:client) { Cronofy::Client.new(access_token: access_token, client_secret: client_secret) }
1817
+ let(:request_headers) do
1818
+ {
1819
+ "Authorization" => "Bearer #{access_token}",
1820
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
1821
+ "Content-Type" => "application/json; charset=utf-8",
1822
+ }
1823
+ end
1824
+
1825
+ let(:participants) do
1826
+ { members: %w{acc_567236000909002 acc_678347111010113} }
1827
+ end
1828
+
1829
+ let(:required_duration) { 60 }
1830
+
1831
+ let(:available_periods) do
1832
+ [
1833
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1834
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1835
+ ]
1836
+ end
1837
+
1838
+ describe "it prefers the access_token for backward compatibility" do
1839
+ it_behaves_like 'a Cronofy request'
1840
+ it_behaves_like 'a Cronofy request with mapped return value'
1841
+ end
1842
+ end
1843
+
1844
+ context "when trying to auth without a client_secret or access_token" do
1845
+ let(:client) { Cronofy::Client.new }
1846
+
1847
+ let(:participants) do
1848
+ { members: %w{acc_567236000909002 acc_678347111010113} }
1849
+ end
1850
+
1851
+ let(:required_duration) { 60 }
1852
+
1853
+ let(:available_periods) do
1854
+ [
1855
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
1856
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
1857
+ ]
1858
+ end
1859
+
1860
+
1861
+ it "raises an API Key error" do
1862
+ expect{ subject }.to raise_error(Cronofy::CredentialsMissingError)
1863
+ end
1864
+ end
1865
+ end
1866
+ end
1867
+
1868
+ describe 'Sequenced Availability' do
1869
+ describe '#sequenced_availability' do
1870
+ let(:method) { :post }
1871
+ let(:request_url) { 'https://api.cronofy.com/v1/sequenced_availability' }
1872
+ let(:request_headers) { json_request_headers }
1873
+
1874
+ let(:client_id) { 'example_id' }
1875
+ let(:client_secret) { 'example_secret' }
1876
+ let(:token) { client_secret }
1877
+
1878
+ let(:client) do
1879
+ Cronofy::Client.new(
1880
+ client_id: client_id,
1881
+ client_secret: client_secret,
1882
+ )
1883
+ end
1884
+
1885
+ let(:request_body) do
1886
+ {
1887
+ "sequence" => [
1888
+ {
1889
+ "sequence_id" => 1234,
1890
+ "ordinal" => 1,
1891
+ "participants" => [
1892
+ {
1893
+ "members" => [
1894
+ { "sub" => "acc_567236000909002" },
1895
+ { "sub" => "acc_678347111010113" }
1896
+ ],
1897
+ "required" => "all"
1898
+ }
1899
+ ],
1900
+ "required_duration" => { "minutes" => 60 },
1901
+ "start_interval" => { "minutes" => 60 },
1902
+ "buffer" => {
1903
+ "before": {
1904
+ "minimum": { "minutes" => 30 },
1905
+ "maximum": { "minutes" => 45 },
1906
+ },
1907
+ "after": {
1908
+ "minimum": { "minutes" => 45 },
1909
+ "maximum": { "minutes" => 60 },
1910
+ },
1911
+ }
1912
+ },
1913
+ {
1914
+ "sequence_id" => 4567,
1915
+ "ordinal" => 2,
1916
+ "participants" => [
1917
+ {
1918
+ "members" => [
1919
+ { "sub" => "acc_567236000909002" },
1920
+ { "sub" => "acc_678347111010113" }
1921
+ ],
1922
+ "required" => "all"
1923
+ }
1924
+ ],
1925
+ "required_duration" => { "minutes" => 60 },
1926
+ }
1927
+ ],
1928
+ "available_periods" => [
1929
+ {
1930
+ "start" => "2017-01-03T09:00:00Z",
1931
+ "end" => "2017-01-03T18:00:00Z"
1932
+ },
1933
+ {
1934
+ "start" => "2017-01-04T09:00:00Z",
1935
+ "end" => "2017-01-04T18:00:00Z"
1936
+ }
1937
+ ]
1938
+ }
1939
+ end
1940
+
1941
+ let(:correct_response_code) { 200 }
1942
+ let(:correct_response_body) do
1943
+ {
1944
+ "sequences" => [
1945
+ {
1946
+ "sequence" => [
1947
+ {
1948
+ "sequence_id" => 1234,
1949
+ "start" => "2017-01-03T09:00:00Z",
1950
+ "end" => "2017-01-03T11:00:00Z",
1951
+ "participants" => [
1952
+ { "sub" => "acc_567236000909002" },
1953
+ { "sub" => "acc_678347111010113" }
1954
+ ]
1955
+ },
1956
+ {
1957
+ "sequence_id" => 4567,
1958
+ "start" => "2017-01-03T14:00:00Z",
1959
+ "end" => "2017-01-03T16:00:00Z",
1960
+ "participants" => [
1961
+ { "sub" => "acc_567236000909002" },
1962
+ { "sub" => "acc_678347111010113" }
1963
+ ]
1964
+ },
1965
+ ]
1966
+ }
1967
+ ]
1968
+ }
1969
+ end
1970
+
1971
+ let(:correct_mapped_result) do
1972
+ correct_response_body['sequences'].map { |sequence| Cronofy::Sequence.new(sequence) }
1973
+ end
1974
+
1975
+ let(:args) do
1976
+ {
1977
+ sequence: [{
1978
+ sequence_id: 1234,
1979
+ ordinal: 1,
1980
+ participants: participants,
1981
+ required_duration: required_duration,
1982
+ start_interval: { minutes: 60 },
1983
+ buffer: {
1984
+ before: {
1985
+ minimum: { minutes: 30 },
1986
+ maximum: { minutes: 45 },
1987
+ },
1988
+ after: {
1989
+ minimum: { minutes: 45 },
1990
+ maximum: { minutes: 60 },
1991
+ },
1992
+ },
1993
+ },
1994
+ {
1995
+ sequence_id: 4567,
1996
+ ordinal: 2,
1997
+ participants: participants,
1998
+ required_duration: required_duration,
1999
+ }],
2000
+ available_periods: available_periods
2001
+ }
2002
+ end
2003
+
2004
+ subject { client.sequenced_availability(args) }
2005
+
2006
+ let(:participants) do
2007
+ [
2008
+ {
2009
+ members: [
2010
+ { sub: "acc_567236000909002" },
2011
+ { sub: "acc_678347111010113" },
2012
+ ],
2013
+ required: :all,
2014
+ }
2015
+ ]
2016
+ end
2017
+
2018
+ let(:required_duration) do
2019
+ { minutes: 60 }
2020
+ end
2021
+
2022
+ let(:available_periods) do
2023
+ [
2024
+ { start: Time.parse("2017-01-03T09:00:00Z"), end: Time.parse("2017-01-03T18:00:00Z") },
2025
+ { start: Time.parse("2017-01-04T09:00:00Z"), end: Time.parse("2017-01-04T18:00:00Z") },
2026
+ ]
2027
+ end
2028
+
2029
+ it_behaves_like 'a Cronofy request'
2030
+ it_behaves_like 'a Cronofy request with mapped return value'
2031
+
2032
+ context "when passing query_periods instead" do
2033
+ before do
2034
+ args[:query_periods] = args[:available_periods]
2035
+ args.delete(:available_periods)
2036
+ request_body["query_periods"] = request_body["available_periods"]
2037
+ request_body.delete("available_periods")
2038
+ end
2039
+
2040
+ it_behaves_like 'a Cronofy request'
2041
+ it_behaves_like 'a Cronofy request with mapped return value'
2042
+ end
2043
+
2044
+ context "when trying to auth with access_token only" do
2045
+ let(:access_token) { "access_token_123"}
2046
+ let(:client) { Cronofy::Client.new(access_token: access_token) }
2047
+ let(:request_headers) do
2048
+ {
2049
+ "Authorization" => "Bearer #{access_token}",
2050
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2051
+ "Content-Type" => "application/json; charset=utf-8",
2052
+ }
2053
+ end
2054
+
2055
+ it_behaves_like 'a Cronofy request'
2056
+ it_behaves_like 'a Cronofy request with mapped return value'
2057
+ end
2058
+
2059
+ context "when trying to auth with both access_token and client_secret provided" do
2060
+ let(:client_id) { 'example_id' }
2061
+ let(:client_secret) { 'example_secret' }
2062
+ let(:access_token) { "access_token_123"}
2063
+
2064
+ let(:client) do
2065
+ Cronofy::Client.new(
2066
+ client_id: client_id,
2067
+ client_secret: client_secret,
2068
+ access_token: access_token,
2069
+ )
2070
+ end
2071
+ let(:request_headers) do
2072
+ {
2073
+ "Authorization" => "Bearer #{access_token}",
2074
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2075
+ "Content-Type" => "application/json; charset=utf-8",
2076
+ }
2077
+ end
2078
+
2079
+ it_behaves_like 'a Cronofy request'
2080
+ it_behaves_like 'a Cronofy request with mapped return value'
2081
+ end
2082
+ end
2083
+ end
2084
+
2085
+ describe "Add to calendar" do
2086
+ let(:request_url) { "https://api.cronofy.com/v1/add_to_calendar" }
2087
+ let(:url) { URI("https://example.com") }
2088
+ let(:method) { :post }
2089
+ let(:request_headers) { json_request_headers }
2090
+
2091
+ let(:start_datetime) { Time.utc(2014, 8, 5, 15, 30, 0) }
2092
+ let(:end_datetime) { Time.utc(2014, 8, 5, 17, 0, 0) }
2093
+ let(:encoded_start_datetime) { "2014-08-05T15:30:00Z" }
2094
+ let(:encoded_end_datetime) { "2014-08-05T17:00:00Z" }
2095
+ let(:location) { { :description => "Board room" } }
2096
+ let(:transparency) { nil }
2097
+ let(:client_id) { 'example_id' }
2098
+ let(:client_secret) { 'example_secret' }
2099
+ let(:scope) { 'read_events delete_events' }
2100
+ let(:state) { 'example_state' }
2101
+ let(:redirect_uri) { 'http://example.com/redirect' }
2102
+
2103
+ let(:client) do
2104
+ Cronofy::Client.new(
2105
+ client_id: client_id,
2106
+ client_secret: client_secret,
2107
+ access_token: token,
2108
+ )
2109
+ end
2110
+
2111
+ let(:event) do
2112
+ {
2113
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2114
+ :summary => "Board meeting",
2115
+ :description => "Discuss plans for the next quarter.",
2116
+ :start => start_datetime,
2117
+ :end => end_datetime,
2118
+ :url => url,
2119
+ :location => location,
2120
+ :transparency => transparency,
2121
+ :reminders => [
2122
+ { :minutes => 60 },
2123
+ { :minutes => 0 },
2124
+ { :minutes => 10 },
2125
+ ],
2126
+ }
2127
+ end
2128
+
2129
+ let(:oauth_body) do
2130
+ {
2131
+ scope: scope,
2132
+ redirect_uri: redirect_uri,
2133
+ state: state,
2134
+ }
2135
+ end
2136
+
2137
+ let(:args) do
2138
+ {
2139
+ oauth: oauth_body,
2140
+ event: event,
2141
+ target_calendars: target_calendars,
2142
+ }
2143
+ end
2144
+
2145
+ let(:target_calendars) do
2146
+ [
2147
+ {
2148
+ sub: "acc_567236000909002",
2149
+ calendar_id: "cal_n23kjnwrw2_jsdfjksn234",
2150
+ }
2151
+ ]
2152
+ end
2153
+
2154
+ let(:request_body) do
2155
+ {
2156
+ client_id: client_id,
2157
+ client_secret: client_secret,
2158
+ oauth: oauth_body,
2159
+ event: {
2160
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2161
+ :summary => "Board meeting",
2162
+ :description => "Discuss plans for the next quarter.",
2163
+ :start => encoded_start_datetime,
2164
+ :end => encoded_end_datetime,
2165
+ :url => url.to_s,
2166
+ :location => location,
2167
+ :transparency => transparency,
2168
+ :reminders => [
2169
+ { :minutes => 60 },
2170
+ { :minutes => 0 },
2171
+ { :minutes => 10 },
2172
+ ],
2173
+ },
2174
+ target_calendars: target_calendars,
2175
+ }
2176
+ end
2177
+ let(:correct_response_code) { 202 }
2178
+ let(:correct_response_body) do
2179
+ {
2180
+ oauth_url: "http://www.example.com/oauth?token=example"
2181
+ }
2182
+ end
2183
+
2184
+ subject { client.add_to_calendar(args) }
2185
+
2186
+ context 'when start/end are Times' do
2187
+ it_behaves_like 'a Cronofy request'
2188
+ end
2189
+
2190
+ end
2191
+
2192
+ describe "Real time scheduling" do
2193
+ let(:client_id) { 'example_id' }
2194
+ let(:client_secret) { 'example_secret' }
2195
+ let(:client) do
2196
+ Cronofy::Client.new(
2197
+ client_id: client_id,
2198
+ client_secret: client_secret,
2199
+ )
2200
+ end
2201
+
2202
+ let(:url) { URI("https://example.com") }
2203
+ let(:location) { { :description => "Board room" } }
2204
+ let(:transparency) { nil }
2205
+ let(:event) do
2206
+ {
2207
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2208
+ :summary => "Board meeting",
2209
+ :description => "Discuss plans for the next quarter.",
2210
+ :url => url,
2211
+ :location => location,
2212
+ :transparency => transparency,
2213
+ :reminders => [
2214
+ { :minutes => 60 },
2215
+ { :minutes => 0 },
2216
+ { :minutes => 10 },
2217
+ ],
2218
+ }
2219
+ end
2220
+
2221
+ let(:real_time_scheduling_status_response) do
2222
+ {
2223
+ real_time_scheduling: {
2224
+ real_time_scheduling_id: 'example_id',
2225
+ url: 'https://app.cronofy.com/real_time_scheduling/example_token',
2226
+ event: event,
2227
+ status: 'disabled',
2228
+ }
2229
+ }
2230
+ end
2231
+
2232
+ describe "#real_time_scheduling" do
2233
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_scheduling" }
2234
+
2235
+ let(:method) { :post }
2236
+
2237
+ let(:request_headers) do
2238
+ {
2239
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2240
+ "Content-Type" => "application/json; charset=utf-8",
2241
+ }
2242
+ end
2243
+
2244
+ let(:scope) { 'read_events delete_events' }
2245
+ let(:state) { 'example_state' }
2246
+ let(:redirect_uri) { 'http://example.com/redirect' }
2247
+
2248
+ let(:oauth_body) do
2249
+ {
2250
+ scope: scope,
2251
+ redirect_uri: redirect_uri,
2252
+ state: state,
2253
+ }
2254
+ end
2255
+
2256
+ let(:target_calendars) do
2257
+ [
2258
+ {
2259
+ sub: "acc_567236000909002",
2260
+ calendar_id: "cal_n23kjnwrw2_jsdfjksn234",
2261
+ }
2262
+ ]
2263
+ end
2264
+
2265
+ let(:availability) do
2266
+ {
2267
+ participants: [
2268
+ {
2269
+ members: [{
2270
+ sub: "acc_567236000909002",
2271
+ calendar_ids: ["cal_n23kjnwrw2_jsdfjksn234"]
2272
+ }],
2273
+ required: 'all'
2274
+ }
2275
+ ],
2276
+ required_duration: { minutes: 60 },
2277
+ available_periods: [{
2278
+ start: Time.utc(2017, 1, 1, 9, 00),
2279
+ end: Time.utc(2017, 1, 1, 17, 00),
2280
+ }],
2281
+ start_interval: { minutes: 60 },
2282
+ buffer: {
2283
+ before: { minutes: 30 },
2284
+ after: { minutes: 45 },
2285
+ }
2286
+ }
2287
+ end
2288
+
2289
+ let(:mapped_availability) do
2290
+ {
2291
+ participants: [
2292
+ {
2293
+ members: [{
2294
+ sub: "acc_567236000909002",
2295
+ calendar_ids: ["cal_n23kjnwrw2_jsdfjksn234"]
2296
+ }],
2297
+ required: 'all'
2298
+ }
2299
+ ],
2300
+ required_duration: { minutes: 60 },
2301
+ start_interval: { minutes: 60 },
2302
+ buffer: {
2303
+ before: { minutes: 30 },
2304
+ after: { minutes: 45 },
2305
+ },
2306
+ available_periods: [{
2307
+ start: "2017-01-01T09:00:00Z",
2308
+ end: "2017-01-01T17:00:00Z",
2309
+ }]
2310
+ }
2311
+ end
2312
+
2313
+ let(:args) do
2314
+ {
2315
+ oauth: oauth_body,
2316
+ event: event,
2317
+ target_calendars: target_calendars,
2318
+ availability: availability,
2319
+ }
2320
+ end
2321
+
2322
+ let(:request_body) do
2323
+ {
2324
+ client_id: client_id,
2325
+ client_secret: client_secret,
2326
+ oauth: oauth_body,
2327
+ event: {
2328
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2329
+ :summary => "Board meeting",
2330
+ :description => "Discuss plans for the next quarter.",
2331
+ :url => url.to_s,
2332
+ :location => location,
2333
+ :transparency => transparency,
2334
+ :reminders => [
2335
+ { :minutes => 60 },
2336
+ { :minutes => 0 },
2337
+ { :minutes => 10 },
2338
+ ],
2339
+ },
2340
+ target_calendars: target_calendars,
2341
+ availability: mapped_availability,
2342
+ }
2343
+ end
2344
+ let(:correct_response_code) { 202 }
2345
+ let(:correct_response_body) do
2346
+ {
2347
+ oauth_url: "http://www.example.com/oauth?token=example"
2348
+ }
2349
+ end
2350
+
2351
+ subject { client.real_time_scheduling(args) }
2352
+
2353
+ context 'when start/end are Times' do
2354
+ it_behaves_like 'a Cronofy request'
2355
+ end
2356
+
2357
+ context 'when passing query periods' do
2358
+ it_behaves_like 'a Cronofy request'
2359
+
2360
+ before do
2361
+ availability[:query_periods] = availability.delete(:available_periods)
2362
+ mapped_availability[:query_periods] = mapped_availability.delete(:available_periods)
2363
+ end
2364
+ end
2365
+ end
2366
+
2367
+ describe "#get_real_time_scheduling_status" do
2368
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_scheduling" }
2369
+ let(:method) { :get }
2370
+ let(:url) { "https://example.com" }
2371
+ let(:request_headers) do
2372
+ {
2373
+ "Authorization" => "Bearer #{client_secret}",
2374
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2375
+ "Accept" => "*/*",
2376
+ "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
2377
+ }
2378
+ end
2379
+ let(:correct_response_code) { 200 }
2380
+ let(:correct_response_body) { real_time_scheduling_status_response }
2381
+ let(:correct_mapped_result) do
2382
+ Cronofy::RealTimeSchedulingStatus.new(correct_response_body[:real_time_scheduling])
2383
+ end
2384
+
2385
+ subject { client.get_real_time_scheduling_status(args) }
2386
+
2387
+ context 'when passing id' do
2388
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_scheduling/example_id" }
2389
+
2390
+ let(:args) do
2391
+ {
2392
+ id: 'example_id'
2393
+ }
2394
+ end
2395
+
2396
+ it_behaves_like 'a Cronofy request'
2397
+ it_behaves_like 'a Cronofy request with mapped return value'
2398
+ end
2399
+
2400
+ context 'when passing token' do
2401
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_scheduling?token=example_token" }
2402
+ let(:args) do
2403
+ {
2404
+ token: 'example_token'
2405
+ }
2406
+ end
2407
+
2408
+ it_behaves_like 'a Cronofy request'
2409
+ it_behaves_like 'a Cronofy request with mapped return value'
2410
+ end
2411
+
2412
+ context 'when passing neither id nor token' do
2413
+ let(:args) { Hash.new }
2414
+ it 'raises an ArgumentError' do
2415
+ expect { subject }.to raise_error(ArgumentError)
2416
+ end
2417
+ end
2418
+ end
2419
+
2420
+ describe "#disable_real_time_scheduling" do
2421
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_scheduling/example_id/disable" }
2422
+ let(:url) { "https://example.com" }
2423
+ let(:method) { :post }
2424
+ let(:request_headers) do
2425
+ {
2426
+ "Authorization" => "Bearer #{client_secret}",
2427
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2428
+ "Content-Type" => "application/json; charset=utf-8",
2429
+ }
2430
+ end
2431
+
2432
+ let(:args) do
2433
+ {
2434
+ id: 'example_id',
2435
+ display_message: 'example message'
2436
+ }
2437
+ end
2438
+
2439
+ let(:request_body) do
2440
+ {
2441
+ display_message: 'example message'
2442
+ }
2443
+ end
2444
+
2445
+ let(:correct_response_code) { 200 }
2446
+ let(:correct_response_body) { real_time_scheduling_status_response }
2447
+ let(:correct_mapped_result) do
2448
+ Cronofy::RealTimeSchedulingStatus.new(correct_response_body[:real_time_scheduling])
2449
+ end
2450
+
2451
+ subject { client.disable_real_time_scheduling(args) }
2452
+
2453
+ it_behaves_like 'a Cronofy request'
2454
+ it_behaves_like 'a Cronofy request with mapped return value'
2455
+
2456
+ context 'when omitting id argument' do
2457
+ let(:args) do
2458
+ {
2459
+ display_message: 'example message'
2460
+ }
2461
+ end
2462
+
2463
+ it 'raises an ArgumentError' do
2464
+ expect { subject }.to raise_error(ArgumentError)
2465
+ end
2466
+ end
2467
+ end
2468
+ end
2469
+
2470
+ describe "Real time sequencing" do
2471
+ let(:request_url) { "https://api.cronofy.com/v1/real_time_sequencing" }
2472
+ let(:url) { URI("https://example.com") }
2473
+ let(:method) { :post }
2474
+
2475
+ let(:request_headers) do
2476
+ {
2477
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2478
+ "Content-Type" => "application/json; charset=utf-8",
2479
+ }
2480
+ end
2481
+
2482
+ let(:location) { { :description => "Board room" } }
2483
+ let(:transparency) { nil }
2484
+ let(:client_id) { 'example_id' }
2485
+ let(:client_secret) { 'example_secret' }
2486
+ let(:scope) { 'read_events delete_events' }
2487
+ let(:state) { 'example_state' }
2488
+ let(:redirect_uri) { 'http://example.com/redirect' }
2489
+
2490
+ let(:client) do
2491
+ Cronofy::Client.new(
2492
+ client_id: client_id,
2493
+ client_secret: client_secret,
2494
+ )
2495
+ end
2496
+
2497
+ let(:event) do
2498
+ {
2499
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2500
+ :summary => "Board meeting",
2501
+ :description => "Discuss plans for the next quarter.",
2502
+ :url => url,
2503
+ :location => location,
2504
+ :transparency => transparency,
2505
+ :reminders => [
2506
+ { :minutes => 60 },
2507
+ { :minutes => 0 },
2508
+ { :minutes => 10 },
2509
+ ],
2510
+ }
2511
+ end
2512
+
2513
+ let(:oauth_body) do
2514
+ {
2515
+ scope: scope,
2516
+ redirect_uri: redirect_uri,
2517
+ state: state,
2518
+ }
2519
+ end
2520
+
2521
+ let(:target_calendars) do
2522
+ [
2523
+ {
2524
+ sub: "acc_567236000909002",
2525
+ calendar_id: "cal_n23kjnwrw2_jsdfjksn234",
2526
+ }
2527
+ ]
2528
+ end
2529
+
2530
+ let(:availability) do
2531
+ {
2532
+ sequence: [
2533
+ {
2534
+ participants: [
2535
+ {
2536
+ members: [{
2537
+ sub: "acc_567236000909002",
2538
+ calendar_ids: ["cal_n23kjnwrw2_jsdfjksn234"]
2539
+ }],
2540
+ required: 'all'
2541
+ }
2542
+ ],
2543
+ required_duration: { minutes: 60 },
2544
+ start_interval: { minutes: 60 },
2545
+ available_periods: [{
2546
+ start: Time.utc(2017, 1, 1, 9, 00),
2547
+ end: Time.utc(2017, 1, 1, 17, 00),
2548
+ }],
2549
+ event: event,
2550
+ buffer: {
2551
+ before: {
2552
+ minimum: { minutes: 30 },
2553
+ maximum: { minutes: 30 },
2554
+ },
2555
+ after: {
2556
+ minimum: { minutes: 30 },
2557
+ maximum: { minutes: 30 },
2558
+ }
2559
+ }
2560
+ }
2561
+ ],
2562
+ available_periods: [{
2563
+ start: Time.utc(2017, 1, 1, 9, 00),
2564
+ end: Time.utc(2017, 1, 1, 17, 00),
2565
+ }],
2566
+ }
2567
+ end
2568
+
2569
+ let(:mapped_availability) do
2570
+ {
2571
+ sequence: [
2572
+ {
2573
+ participants: [
2574
+ {
2575
+ members: [{
2576
+ sub: "acc_567236000909002",
2577
+ calendar_ids: ["cal_n23kjnwrw2_jsdfjksn234"]
2578
+ }],
2579
+ required: 'all'
2580
+ }
2581
+ ],
2582
+ required_duration: { minutes: 60 },
2583
+ start_interval: { minutes: 60 },
2584
+ available_periods: [{
2585
+ start: "2017-01-01T09:00:00Z",
2586
+ end: "2017-01-01T17:00:00Z",
2587
+ }],
2588
+ event: mapped_event,
2589
+ buffer: {
2590
+ before: {
2591
+ minimum: { minutes: 30 },
2592
+ maximum: { minutes: 30 },
2593
+ },
2594
+ after: {
2595
+ minimum: { minutes: 30 },
2596
+ maximum: { minutes: 30 },
2597
+ }
2598
+ }
2599
+ }
2600
+ ],
2601
+ available_periods: [{
2602
+ start: "2017-01-01T09:00:00Z",
2603
+ end: "2017-01-01T17:00:00Z",
2604
+ }]
2605
+ }
2606
+ end
2607
+
2608
+ let(:mapped_event) do
2609
+ {
2610
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
2611
+ :summary => "Board meeting",
2612
+ :description => "Discuss plans for the next quarter.",
2613
+ :url => url.to_s,
2614
+ :location => location,
2615
+ :transparency => transparency,
2616
+ :reminders => [
2617
+ { :minutes => 60 },
2618
+ { :minutes => 0 },
2619
+ { :minutes => 10 },
2620
+ ],
2621
+ }
2622
+ end
2623
+
2624
+ let(:args) do
2625
+ {
2626
+ oauth: oauth_body,
2627
+ event: event,
2628
+ target_calendars: target_calendars,
2629
+ availability: availability,
2630
+ }
2631
+ end
2632
+
2633
+ let(:request_body) do
2634
+ {
2635
+ client_id: client_id,
2636
+ client_secret: client_secret,
2637
+ oauth: oauth_body,
2638
+ event: mapped_event,
2639
+ target_calendars: target_calendars,
2640
+ availability: mapped_availability,
2641
+ }
2642
+ end
2643
+ let(:correct_response_code) { 202 }
2644
+ let(:correct_response_body) do
2645
+ {
2646
+ oauth_url: "http://www.example.com/oauth?token=example"
2647
+ }
2648
+ end
2649
+
2650
+ subject { client.real_time_sequencing(args) }
2651
+
2652
+ context 'when start/end are Times' do
2653
+ it_behaves_like 'a Cronofy request'
2654
+ end
2655
+
2656
+ context 'when passing query periods' do
2657
+ it_behaves_like 'a Cronofy request'
2658
+
2659
+ before do
2660
+ availability[:query_periods] = availability.delete(:available_periods)
2661
+ availability[:sequence].each do |sequence|
2662
+ sequence[:query_periods] = sequence.delete(:available_periods)
2663
+ end
2664
+
2665
+ mapped_availability[:query_periods] = mapped_availability.delete(:available_periods)
2666
+ mapped_availability[:sequence].each do |sequence|
2667
+ sequence[:query_periods] = sequence.delete(:available_periods)
2668
+ end
2669
+ end
2670
+ end
2671
+ end
2672
+
2673
+ describe "specifying data_centre" do
2674
+ let(:data_center) { :de }
2675
+
2676
+ let(:client) do
2677
+ Cronofy::Client.new(
2678
+ client_id: 'client_id_123',
2679
+ client_secret: 'client_secret_456',
2680
+ access_token: token,
2681
+ refresh_token: 'refresh_token_456',
2682
+ data_centre: data_center,
2683
+ )
2684
+ end
2685
+
2686
+ describe "Userinfo" do
2687
+ let(:request_url) { "https://api-#{data_center}.cronofy.com/v1/userinfo" }
2688
+
2689
+ describe "#userinfo" do
2690
+ let(:method) { :get }
2691
+
2692
+ let(:correct_response_code) { 200 }
2693
+ let(:correct_response_body) do
2694
+ {
2695
+ "sub" => "ser_5700a00eb0ccd07000000000",
2696
+ "cronofy.type" => "service_account",
2697
+ "cronofy.service_account.domain" => "example.com"
2698
+ }
2699
+ end
2700
+
2701
+ let(:correct_mapped_result) do
2702
+ Cronofy::UserInfo.new(correct_response_body)
2703
+ end
2704
+
2705
+ subject { client.userinfo }
2706
+
2707
+ it_behaves_like "a Cronofy request"
2708
+ it_behaves_like "a Cronofy request with mapped return value"
2709
+ end
2710
+ end
2711
+ end
2712
+
2713
+ describe "specifying data_center" do
2714
+ let(:data_center) { :au }
2715
+
2716
+ let(:client) do
2717
+ Cronofy::Client.new(
2718
+ client_id: 'client_id_123',
2719
+ client_secret: 'client_secret_456',
2720
+ access_token: token,
2721
+ refresh_token: 'refresh_token_456',
2722
+ data_center: data_center,
2723
+ )
2724
+ end
2725
+
2726
+ describe "Userinfo" do
2727
+ let(:request_url) { "https://api-#{data_center}.cronofy.com/v1/userinfo" }
2728
+
2729
+ describe "#userinfo" do
2730
+ let(:method) { :get }
2731
+
2732
+ let(:correct_response_code) { 200 }
2733
+ let(:correct_response_body) do
2734
+ {
2735
+ "sub" => "ser_5700a00eb0ccd07000000000",
2736
+ "cronofy.type" => "service_account",
2737
+ "cronofy.service_account.domain" => "example.com"
2738
+ }
2739
+ end
2740
+
2741
+ let(:correct_mapped_result) do
2742
+ Cronofy::UserInfo.new(correct_response_body)
2743
+ end
2744
+
2745
+ subject { client.userinfo }
2746
+
2747
+ it_behaves_like "a Cronofy request"
2748
+ it_behaves_like "a Cronofy request with mapped return value"
2749
+ end
2750
+ end
2751
+ end
2752
+
2753
+ describe "HMAC verification" do
2754
+ let(:client) do
2755
+ Cronofy::Client.new(
2756
+ client_secret: 'pDY0Oi7TJSP2hfNmZNkm5',
2757
+ access_token: token,
2758
+ refresh_token: 'refresh_token_456',
2759
+ )
2760
+ end
2761
+
2762
+ let(:body) { "{\"example\":\"well-known\"}" }
2763
+
2764
+ it "verifies the correct HMAC" do
2765
+ expect(client.hmac_valid?(body: body, hmac: "6r2/HjBkqymGegX0wOfifieeUXbbHwtV/LohHS+jv6c=")).to be true
2766
+ end
2767
+
2768
+ it "rejects an incorrect HMAC" do
2769
+ expect(client.hmac_valid?(body: body, hmac: "something-else")).to be false
2770
+ end
2771
+
2772
+ it "verifies the correct HMAC when one of the multiple HMACs splitted by ',' match" do
2773
+ expect(client.hmac_valid?(body: body, hmac: "6r2/HjBkqymGegX0wOfifieeUXbbHwtV/LohHS+jv6c=,something-else")).to be true
2774
+ end
2775
+
2776
+ it "rejects incorrect when multiple HMACs splitted by ',' don't match" do
2777
+ expect(client.hmac_valid?(body: body, hmac: "something-else,something-else2")).to be false
2778
+ end
2779
+
2780
+ it "rejects if empty HMAC" do
2781
+ expect(client.hmac_valid?(body: body, hmac: "")).to be false
2782
+ end
2783
+
2784
+ it "rejects if nil HMAC" do
2785
+ expect(client.hmac_valid?(body: body, hmac: nil)).to be false
2786
+ end
2787
+ end
2788
+
2789
+ describe "Smart Invite" do
2790
+ let(:request_url) { "https://api.cronofy.com/v1/smart_invites" }
2791
+ let(:url) { URI("https://example.com") }
2792
+ let(:method) { :post }
2793
+
2794
+ let(:request_headers) do
2795
+ {
2796
+ "Authorization" => "Bearer #{client_secret}",
2797
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2798
+ "Content-Type" => "application/json; charset=utf-8",
2799
+ }
2800
+ end
2801
+
2802
+ let(:location) { { :description => "Board room" } }
2803
+ let(:client_id) { 'example_id' }
2804
+ let(:client_secret) { 'example_secret' }
2805
+
2806
+ let(:client) do
2807
+ Cronofy::Client.new(
2808
+ client_id: client_id,
2809
+ client_secret: client_secret,
2810
+ )
2811
+ end
2812
+
2813
+ let(:start_datetime) { Time.utc(2014, 8, 5, 15, 30, 0) }
2814
+ let(:end_datetime) { Time.utc(2014, 8, 5, 17, 0, 0) }
2815
+ let(:encoded_start_datetime) { "2014-08-05T15:30:00Z" }
2816
+ let(:encoded_end_datetime) { "2014-08-05T17:00:00Z" }
2817
+
2818
+ let(:args) do
2819
+ {
2820
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
2821
+ callback_url: url.to_s,
2822
+ event: {
2823
+ :summary => "Board meeting",
2824
+ :description => "Discuss plans for the next quarter.",
2825
+ :url => url.to_s,
2826
+ :start => start_datetime,
2827
+ :end => encoded_end_datetime,
2828
+ :location => location,
2829
+ :reminders => [
2830
+ { :minutes => 60 },
2831
+ { :minutes => 0 },
2832
+ { :minutes => 10 },
2833
+ ],
2834
+ },
2835
+ recipient: {
2836
+ email: "example@example.com"
2837
+ }
2838
+ }
2839
+ end
2840
+
2841
+ let(:request_body) do
2842
+ {
2843
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
2844
+ callback_url: url.to_s,
2845
+ event: {
2846
+ :summary => "Board meeting",
2847
+ :description => "Discuss plans for the next quarter.",
2848
+ :url => url.to_s,
2849
+ :start => encoded_start_datetime,
2850
+ :end => encoded_end_datetime,
2851
+ :location => location,
2852
+ :reminders => [
2853
+ { :minutes => 60 },
2854
+ { :minutes => 0 },
2855
+ { :minutes => 10 },
2856
+ ],
2857
+ },
2858
+ recipient: {
2859
+ email: "example@example.com"
2860
+ }
2861
+ }
2862
+ end
2863
+ let(:correct_response_code) { 202 }
2864
+ let(:correct_response_body) do
2865
+ request_body.merge({
2866
+ attachments: []
2867
+ })
2868
+ end
2869
+
2870
+ subject { client.upsert_smart_invite(request_body) }
2871
+
2872
+ it_behaves_like 'a Cronofy request'
2873
+
2874
+ end
2875
+
2876
+ describe 'Read smart invite' do
2877
+ before do
2878
+ stub_request(method, request_url)
2879
+ .with(headers: request_headers)
2880
+ .to_return(status: correct_response_code,
2881
+ headers: correct_response_headers,
2882
+ body: correct_response_body.to_json)
2883
+ end
2884
+
2885
+ let(:request_headers) do
2886
+ {
2887
+ "Authorization" => "Bearer #{client_secret}",
2888
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2889
+ "Accept" => "*/*",
2890
+ "Accept-Encoding" => "gzip;q=1.0,deflate;q=0.6,identity;q=0.3",
2891
+ }
2892
+ end
2893
+
2894
+ let(:client_id) { 'example_id' }
2895
+ let(:client_secret) { 'example_secret' }
2896
+
2897
+ let(:client) do
2898
+ Cronofy::Client.new(
2899
+ client_id: client_id,
2900
+ client_secret: client_secret,
2901
+ )
2902
+ end
2903
+
2904
+ let(:request_url_prefix) { 'https://api.cronofy.com/v1/smart_invites' }
2905
+ let(:method) { :get }
2906
+ let(:correct_response_code) { 200 }
2907
+ let(:smart_invite_id) { "smart_invite_id_1234" }
2908
+ let(:recipient_email) { "example@example.com" }
2909
+ let(:request_url) { request_url_prefix + "?recipient_email=#{recipient_email}&smart_invite_id=#{smart_invite_id}" }
2910
+
2911
+ let(:correct_response_body) do
2912
+ {
2913
+ "recipient" => {
2914
+ "email" => recipient_email,
2915
+ "status" => "declined",
2916
+ "comment" => "example comment",
2917
+ "proposal" => {
2918
+ "start" => {
2919
+ "time" => "2014-09-13T23:00:00+02:00",
2920
+ "tzid" => "Europe/Paris"
2921
+ },
2922
+ "end" => {
2923
+ "time" => "2014-09-13T23:00:00+02:00",
2924
+ "tzid" => "Europe/Paris"
2925
+ }
2926
+ }
2927
+ },
2928
+ "replies" => [
2929
+ {
2930
+ "email" => "person1@example.com",
2931
+ "status" => "accepted"
2932
+ },
2933
+ {
2934
+ "email" => "person2@example.com",
2935
+ "status" => "declined",
2936
+ "comment" => "example comment",
2937
+ "proposal" => {
2938
+ "start" => {
2939
+ "time" => "2014-09-13T23:00:00+02:00",
2940
+ "tzid" => "Europe/Paris"
2941
+ },
2942
+ "end" => {
2943
+ "time" => "2014-09-13T23:00:00+02:00",
2944
+ "tzid" => "Europe/Paris"
2945
+ }
2946
+ }
2947
+ }
2948
+ ],
2949
+ "smart_invite_id" => smart_invite_id,
2950
+ "callback_url" => "https =>//example.yourapp.com/cronofy/smart_invite/notifications",
2951
+ "event" => {
2952
+ "summary" => "Board meeting",
2953
+ "description" => "Discuss plans for the next quarter.",
2954
+ "start" => {
2955
+ "time" => "2017-10-05T09:30:00Z",
2956
+ "tzid" => "Europe/London"
2957
+ },
2958
+ "end" => {
2959
+ "time" => "2017-10-05T10:00:00Z",
2960
+ "tzid" => "Europe/London"
2961
+ },
2962
+ "location" => {
2963
+ "description" => "Board room"
2964
+ }
2965
+ }
2966
+ }
2967
+ end
2968
+
2969
+ let(:correct_mapped_result) do
2970
+ Cronofy::SmartInviteResponse.new(correct_response_body)
2971
+ end
2972
+
2973
+ subject do
2974
+ client.get_smart_invite(smart_invite_id, recipient_email)
2975
+ end
2976
+
2977
+ it_behaves_like 'a Cronofy request'
2978
+ it_behaves_like 'a Cronofy request with mapped return value'
2979
+ end
2980
+
2981
+ describe "Cancel Smart Invite" do
2982
+ let(:request_url) { "https://api.cronofy.com/v1/smart_invites" }
2983
+ let(:method) { :post }
2984
+
2985
+ let(:request_headers) do
2986
+ {
2987
+ "Authorization" => "Bearer #{client_secret}",
2988
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
2989
+ "Content-Type" => "application/json; charset=utf-8",
2990
+ }
2991
+ end
2992
+
2993
+ let(:client_id) { 'example_id' }
2994
+ let(:client_secret) { 'example_secret' }
2995
+
2996
+ let(:client) do
2997
+ Cronofy::Client.new(
2998
+ client_id: client_id,
2999
+ client_secret: client_secret,
3000
+ )
3001
+ end
3002
+
3003
+ let(:args) do
3004
+ {
3005
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
3006
+ recipient: {
3007
+ email: "example@example.com"
3008
+ }
3009
+ }
3010
+ end
3011
+
3012
+ let(:request_body) do
3013
+ {
3014
+ method: 'cancel',
3015
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
3016
+ recipient: {
3017
+ email: "example@example.com"
3018
+ }
3019
+ }
3020
+ end
3021
+ let(:correct_response_code) { 202 }
3022
+ let(:correct_response_body) do
3023
+ request_body.merge({
3024
+ attachments: []
3025
+ })
3026
+ end
3027
+
3028
+ subject { client.cancel_smart_invite(request_body) }
3029
+
3030
+ it_behaves_like 'a Cronofy request'
3031
+
3032
+ end
3033
+
3034
+ describe "Remove Recipient Smart Invite", test: true do
3035
+ let(:request_url) { "https://api.cronofy.com/v1/smart_invites" }
3036
+ let(:method) { :post }
3037
+
3038
+ let(:request_headers) do
3039
+ {
3040
+ "Authorization" => "Bearer #{client_secret}",
3041
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
3042
+ "Content-Type" => "application/json; charset=utf-8",
3043
+ }
3044
+ end
3045
+
3046
+ let(:client_id) { 'example_id' }
3047
+ let(:client_secret) { 'example_secret' }
3048
+
3049
+ let(:client) do
3050
+ Cronofy::Client.new(
3051
+ client_id: client_id,
3052
+ client_secret: client_secret,
3053
+ )
3054
+ end
3055
+
3056
+ let(:args) do
3057
+ {
3058
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
3059
+ recipient: {
3060
+ email: "example@example.com"
3061
+ }
3062
+ }
3063
+ end
3064
+
3065
+ let(:request_body) do
3066
+ {
3067
+ method: 'remove',
3068
+ smart_invite_id: "qTtZdczOccgaPncGJaCiLg",
3069
+ recipient: {
3070
+ email: "example@example.com"
3071
+ }
3072
+ }
3073
+ end
3074
+ let(:correct_response_code) { 202 }
3075
+ let(:correct_response_body) do
3076
+ request_body.merge({
3077
+ attachments: {
3078
+ removed: {
3079
+ email: "example@example.com"
3080
+ }
3081
+ }
3082
+ })
3083
+ end
3084
+
3085
+ let(:correct_mapped_result) do
3086
+ Cronofy::SmartInviteResponse.new(correct_response_body)
3087
+ end
3088
+
3089
+ subject { client.remove_recipient_smart_invite(request_body) }
3090
+
3091
+ it_behaves_like 'a Cronofy request'
3092
+ it_behaves_like 'a Cronofy request with mapped return value'
3093
+ end
3094
+
3095
+ describe "Batch requests" do
3096
+ context "upserting an event" do
3097
+ let(:calendar_id) { 'calendar_id_123'}
3098
+ let(:request_url) { "https://api.cronofy.com/v1/batch" }
3099
+ let(:url) { URI("https://example.com") }
3100
+ let(:method) { :post }
3101
+ let(:request_headers) { json_request_headers }
3102
+
3103
+ let(:start_datetime) { Time.utc(2014, 8, 5, 15, 30, 0) }
3104
+ let(:end_datetime) { Time.utc(2014, 8, 5, 17, 0, 0) }
3105
+ let(:encoded_start_datetime) { "2014-08-05T15:30:00Z" }
3106
+ let(:encoded_end_datetime) { "2014-08-05T17:00:00Z" }
3107
+ let(:location) { { :description => "Board room" } }
3108
+
3109
+ let(:event) do
3110
+ {
3111
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
3112
+ :summary => "Board meeting",
3113
+ :description => "Discuss plans for the next quarter.",
3114
+ :start => start_datetime,
3115
+ :end => end_datetime,
3116
+ :url => url,
3117
+ :location => location,
3118
+ :reminders => [
3119
+ { :minutes => 60 },
3120
+ { :minutes => 0 },
3121
+ { :minutes => 10 },
3122
+ ],
3123
+ }
3124
+ end
3125
+
3126
+ let(:request_body) do
3127
+ {
3128
+ :batch => [
3129
+ {
3130
+ :method => "POST",
3131
+ :relative_url => "/v1/calendars/#{calendar_id}/events",
3132
+ :data => {
3133
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
3134
+ :summary => "Board meeting",
3135
+ :description => "Discuss plans for the next quarter.",
3136
+ :start => encoded_start_datetime,
3137
+ :end => encoded_end_datetime,
3138
+ :url => url.to_s,
3139
+ :location => location,
3140
+ :reminders => [
3141
+ { :minutes => 60 },
3142
+ { :minutes => 0 },
3143
+ { :minutes => 10 },
3144
+ ],
3145
+ }
3146
+ }
3147
+ ]
3148
+ }
3149
+ end
3150
+
3151
+ let(:correct_response_code) { 207 }
3152
+ let(:correct_response_body) do
3153
+ {
3154
+ "batch" => [
3155
+ { "status" => 202 }
3156
+ ]
3157
+ }
3158
+ end
3159
+
3160
+ subject do
3161
+ client.batch do |batch|
3162
+ batch.upsert_event(calendar_id, event)
3163
+ end
3164
+ end
3165
+
3166
+ it_behaves_like "a Cronofy request"
3167
+ end
3168
+
3169
+ context "deleting an event" do
3170
+ let(:calendar_id) { 'calendar_id_123'}
3171
+ let(:method) { :post }
3172
+ let(:request_url) { "https://api.cronofy.com/v1/batch" }
3173
+ let(:request_headers) { json_request_headers }
3174
+
3175
+ let(:event_id) { "asd1knkjsndk123123" }
3176
+
3177
+ let(:request_body) do
3178
+ {
3179
+ :batch => [
3180
+ {
3181
+ :method => "DELETE",
3182
+ :relative_url => "/v1/calendars/#{calendar_id}/events",
3183
+ :data => {
3184
+ :event_id => event_id,
3185
+ }
3186
+ }
3187
+ ]
3188
+ }
3189
+ end
3190
+
3191
+ let(:correct_response_code) { 207 }
3192
+ let(:correct_response_body) do
3193
+ {
3194
+ "batch" => [
3195
+ { "status" => 202 }
3196
+ ]
3197
+ }
3198
+ end
3199
+
3200
+ subject do
3201
+ client.batch do |batch|
3202
+ batch.delete_event(calendar_id, event_id)
3203
+ end
3204
+ end
3205
+
3206
+ it_behaves_like "a Cronofy request"
3207
+ end
3208
+
3209
+ context "deleting an external event" do
3210
+ let(:calendar_id) { 'calendar_id_123'}
3211
+ let(:method) { :post }
3212
+ let(:request_url) { "https://api.cronofy.com/v1/batch" }
3213
+ let(:request_headers) { json_request_headers }
3214
+
3215
+ let(:event_uid) { "evt_external_12345abcde" }
3216
+
3217
+ let(:request_body) do
3218
+ {
3219
+ :batch => [
3220
+ {
3221
+ :method => "DELETE",
3222
+ :relative_url => "/v1/calendars/#{calendar_id}/events",
3223
+ :data => {
3224
+ :event_uid => event_uid,
3225
+ }
3226
+ }
3227
+ ]
3228
+ }
3229
+ end
3230
+
3231
+ let(:correct_response_code) { 207 }
3232
+ let(:correct_response_body) do
3233
+ {
3234
+ "batch" => [
3235
+ { "status" => 202 }
3236
+ ]
3237
+ }
3238
+ end
3239
+
3240
+ subject do
3241
+ client.batch do |batch|
3242
+ batch.delete_external_event(calendar_id, event_uid)
3243
+ end
3244
+ end
3245
+
3246
+ it_behaves_like "a Cronofy request"
3247
+ end
3248
+
3249
+ context "partial success" do
3250
+ let(:method) { :post }
3251
+ let(:request_url) { "https://api.cronofy.com/v1/batch" }
3252
+ let(:request_headers) { json_request_headers }
3253
+
3254
+ let(:request_body) do
3255
+ {
3256
+ :batch => [
3257
+ {
3258
+ :method => "DELETE",
3259
+ :relative_url => "/v1/calendars/cal_123_abc/events",
3260
+ :data => {
3261
+ :event_id => "123",
3262
+ }
3263
+ },
3264
+ {
3265
+ :method => "DELETE",
3266
+ :relative_url => "/v1/calendars/cal_123_def/events",
3267
+ :data => {
3268
+ :event_id => "456",
3269
+ }
3270
+ }
3271
+ ]
3272
+ }
3273
+ end
3274
+
3275
+ let(:correct_response_code) { 207 }
3276
+ let(:correct_response_body) do
3277
+ {
3278
+ "batch" => [
3279
+ { "status" => 202 },
3280
+ { "status" => 404 },
3281
+ ]
3282
+ }
3283
+ end
3284
+
3285
+ subject do
3286
+ client.batch do |batch|
3287
+ batch.delete_event("cal_123_abc", "123")
3288
+ batch.delete_event("cal_123_def", "456")
3289
+ end
3290
+ end
3291
+
3292
+ it "raises an error" do
3293
+ stub_request(method, request_url)
3294
+ .with(headers: request_headers,
3295
+ body: request_body)
3296
+ .to_return(status: correct_response_code,
3297
+ headers: correct_response_headers,
3298
+ body: correct_response_body.to_json)
3299
+
3300
+ expect { subject }.to raise_error(Cronofy::BatchResponse::PartialSuccessError) do |error|
3301
+ expect(error.batch_response.errors?).to be true
3302
+ end
3303
+ end
3304
+ end
3305
+ end
3306
+
3307
+ describe '#create_scheduling_conversation' do
3308
+ let(:request_url) { 'https://api.cronofy.com/v1/scheduling_conversations' }
3309
+ let(:method) { :post }
3310
+ let(:request_body) do
3311
+ {
3312
+ "participants" => [
3313
+ {
3314
+ "participant_id" => "@grace",
3315
+ "sub" => "acc_567236000909002",
3316
+ "slots" => {
3317
+ "choice_method" => "auto"
3318
+ }
3319
+ },
3320
+ {
3321
+ "participant_id" => "@karl"
3322
+ }
3323
+ ],
3324
+ "required_duration" => { "minutes" => 60 },
3325
+ "available_periods" => [
3326
+ {
3327
+ "start" => "2018-05-01T00:00:00Z",
3328
+ "end" => "2018-05-08T23:59:59Z"
3329
+ }
3330
+ ]
3331
+ }
3332
+ end
3333
+
3334
+ let(:correct_response_code) { 200 }
3335
+ let(:correct_response_body) do
3336
+ {
3337
+ "scheduling_conversation" => {
3338
+ "scheduling_conversation_id" => "abcd1234"
3339
+ }
3340
+ }
3341
+ end
3342
+
3343
+ let(:correct_mapped_result) do
3344
+ Cronofy::SchedulingConversation.new(scheduling_conversation_id: "abcd1234")
3345
+ end
3346
+
3347
+ subject { client.create_scheduling_conversation(request_body) }
3348
+
3349
+ it_behaves_like 'a Cronofy request'
3350
+ it_behaves_like 'a Cronofy request with mapped return value'
3351
+ end
3352
+
3353
+ describe '#lookup_scheduling_conversation' do
3354
+ let(:token) { "hagsdau7g3d" }
3355
+ let(:request_url) { "https://api.cronofy.com/v1/scheduling_conversations?token=#{token}" }
3356
+ let(:method) { :get }
3357
+
3358
+ let(:correct_response_code) { 200 }
3359
+ let(:correct_response_body) do
3360
+ {
3361
+ "participant" => {
3362
+ "participant_id" => "83o38hoa"
3363
+ },
3364
+ "scheduling_conversation" => {
3365
+ "scheduling_conversation_id" => "abcd1234"
3366
+ },
3367
+ }
3368
+ end
3369
+
3370
+ let(:correct_mapped_result) do
3371
+ Cronofy::SchedulingConversationResponse.new(
3372
+ participant: Cronofy::Participant.new(participant_id: "83o38hoa"),
3373
+ scheduling_conversation: Cronofy::SchedulingConversation.new(scheduling_conversation_id: "abcd1234"),
3374
+ )
3375
+ end
3376
+
3377
+ subject { client.lookup_scheduling_conversation(token) }
3378
+
3379
+ it_behaves_like 'a Cronofy request'
3380
+ it_behaves_like 'a Cronofy request with mapped return value'
3381
+ end
3382
+
3383
+ describe '#upsert_availability_rule' do
3384
+ let(:request_url) { 'https://api.cronofy.com/v1/availability_rules' }
3385
+ let(:method) { :post }
3386
+ let(:request_body) do
3387
+ {
3388
+ "availability_rule_id" => "default",
3389
+ "tzid" => "America/Chicago",
3390
+ "calendar_ids" => [
3391
+ "cal_n23kjnwrw2_jsdfjksn234"
3392
+ ],
3393
+ "weekly_periods" => [
3394
+ {
3395
+ "day" => "monday",
3396
+ "start_time" => "09:30",
3397
+ "end_time" => "16:30"
3398
+ },
3399
+ {
3400
+ "day" => "wednesday",
3401
+ "start_time" => "09:30",
3402
+ "end_time" => "16:30"
3403
+ }
3404
+ ]
3405
+ }
3406
+ end
3407
+
3408
+ let(:correct_response_code) { 200 }
3409
+ let(:correct_response_body) do
3410
+ {
3411
+ "availability_rule" => {
3412
+ "availability_rule_id" => "default",
3413
+ "tzid" => "America/Chicago",
3414
+ "calendar_ids" => [
3415
+ "cal_n23kjnwrw2_jsdfjksn234"
3416
+ ],
3417
+ "weekly_periods" => [
3418
+ {
3419
+ "day" => "monday",
3420
+ "start_time" => "09:30",
3421
+ "end_time" => "16:30"
3422
+ },
3423
+ {
3424
+ "day" => "wednesday",
3425
+ "start_time" => "09:30",
3426
+ "end_time" => "16:30"
3427
+ }
3428
+ ]
3429
+ }
3430
+ }
3431
+ end
3432
+
3433
+ let(:correct_mapped_result) do
3434
+ Cronofy::AvailabilityRule.new(
3435
+ availability_rule_id: request_body['availability_rule_id'],
3436
+ tzid: request_body['tzid'],
3437
+ calendar_ids: request_body['calendar_ids'],
3438
+ weekly_periods: request_body['weekly_periods'].map { |wp| Cronofy::WeeklyPeriod.new(wp) },
3439
+ )
3440
+ end
3441
+
3442
+ subject { client.upsert_availability_rule(request_body) }
3443
+
3444
+ it_behaves_like 'a Cronofy request'
3445
+ it_behaves_like 'a Cronofy request with mapped return value'
3446
+ end
3447
+
3448
+ describe "#get_availability_rule" do
3449
+ let(:availability_rule_id) { 'default'}
3450
+ let(:request_url) { "https://api.cronofy.com/v1/availability_rules/#{availability_rule_id}" }
3451
+ let(:method) { :get }
3452
+
3453
+ let(:correct_response_code) { 200 }
3454
+ let(:correct_response_body) do
3455
+ {
3456
+ "availability_rule" => {
3457
+ "availability_rule_id" => "default",
3458
+ "tzid" => "America/Chicago",
3459
+ "calendar_ids" => [
3460
+ "cal_n23kjnwrw2_jsdfjksn234"
3461
+ ],
3462
+ "weekly_periods" => [
3463
+ {
3464
+ "day" => "monday",
3465
+ "start_time" => "09:30",
3466
+ "end_time" => "16:30"
3467
+ },
3468
+ {
3469
+ "day" => "wednesday",
3470
+ "start_time" => "09:30",
3471
+ "end_time" => "16:30"
3472
+ }
3473
+ ]
3474
+ }
3475
+ }
3476
+ end
3477
+
3478
+ let(:correct_mapped_result) do
3479
+ rule = correct_response_body['availability_rule']
3480
+ Cronofy::AvailabilityRule.new(
3481
+ availability_rule_id: rule['availability_rule_id'],
3482
+ tzid: rule['tzid'],
3483
+ calendar_ids: rule['calendar_ids'],
3484
+ weekly_periods: rule['weekly_periods'].map { |wp| Cronofy::WeeklyPeriod.new(wp) },
3485
+ )
3486
+ end
3487
+
3488
+ subject { client.get_availability_rule(availability_rule_id) }
3489
+
3490
+ it_behaves_like 'a Cronofy request'
3491
+ it_behaves_like 'a Cronofy request with mapped return value'
3492
+ end
3493
+
3494
+ describe "#get_availability_rules" do
3495
+ let(:request_url) { "https://api.cronofy.com/v1/availability_rules" }
3496
+ let(:method) { :get }
3497
+
3498
+ let(:correct_response_code) { 200 }
3499
+ let(:correct_response_body) do
3500
+ {
3501
+ "availability_rules" => [
3502
+ {
3503
+ "availability_rule_id" => "default",
3504
+ "tzid" => "America/Chicago",
3505
+ "calendar_ids" => [
3506
+ "cal_n23kjnwrw2_jsdfjksn234"
3507
+ ],
3508
+ "weekly_periods" => [
3509
+ {
3510
+ "day" => "monday",
3511
+ "start_time" => "09:30",
3512
+ "end_time" => "16:30"
3513
+ },
3514
+ {
3515
+ "day" => "wednesday",
3516
+ "start_time" => "09:30",
3517
+ "end_time" => "16:30"
3518
+ }
3519
+ ]
3520
+ }
3521
+ ]
3522
+ }
3523
+ end
3524
+
3525
+ let(:correct_mapped_result) do
3526
+ rule = correct_response_body['availability_rules'][0]
3527
+
3528
+ [
3529
+ Cronofy::AvailabilityRule.new(
3530
+ availability_rule_id: rule['availability_rule_id'],
3531
+ tzid: rule['tzid'],
3532
+ calendar_ids: rule['calendar_ids'],
3533
+ weekly_periods: rule['weekly_periods'].map { |wp| Cronofy::WeeklyPeriod.new(wp) },
3534
+ )
3535
+ ]
3536
+ end
3537
+
3538
+ subject { client.get_availability_rules }
3539
+
3540
+ it_behaves_like 'a Cronofy request'
3541
+ it_behaves_like 'a Cronofy request with mapped return value'
3542
+ end
3543
+
3544
+ describe '#delete_availability_rule' do
3545
+ let(:availability_rule_id) { 'default'}
3546
+ let(:request_url) { "https://api.cronofy.com/v1/availability_rules/#{availability_rule_id}" }
3547
+ let(:method) { :delete }
3548
+ let(:request_body) { nil }
3549
+ let(:correct_response_code) { 202 }
3550
+ let(:correct_response_body) { nil }
3551
+
3552
+ subject { client.delete_availability_rule(availability_rule_id) }
3553
+
3554
+ it_behaves_like 'a Cronofy request'
3555
+ end
3556
+
3557
+ describe "#upsert_available_period" do
3558
+ let(:request_url) { 'https://api.cronofy.com/v1/available_periods' }
3559
+ let(:method) { :post }
3560
+ let(:available_period_id) { "test" }
3561
+ let(:request_body) do
3562
+ {
3563
+ available_period_id: available_period_id,
3564
+ start: "2020-07-26T15:30:00Z",
3565
+ end: "2020-07-26T17:00:00Z"
3566
+ }
3567
+ end
3568
+
3569
+ let(:correct_response_code) { 202 }
3570
+ let(:correct_response_body) { "" }
3571
+ let(:correct_mapped_result) { nil }
3572
+
3573
+ subject {
3574
+ client.upsert_available_period(available_period_id,
3575
+ start: request_body[:start],
3576
+ end: request_body[:end]
3577
+ )
3578
+ }
3579
+
3580
+ it_behaves_like 'a Cronofy request'
3581
+ it_behaves_like 'a Cronofy request with mapped return value'
3582
+ end
3583
+
3584
+ describe "#get_available_periods" do
3585
+ context "unfiltered" do
3586
+ let(:request_url) { "https://api.cronofy.com/v1/available_periods" }
3587
+ let(:method) { :get }
3588
+
3589
+ let(:correct_response_code) { 200 }
3590
+ let(:correct_response_body) do
3591
+ {
3592
+ "available_periods" => [
3593
+ {
3594
+ "available_period_id" => "qTtZdczOccgaPncGJaCiLg",
3595
+ "start" => "2020-07-26T15:30:00Z",
3596
+ "end" => "2020-07-26T17:00:00Z"
3597
+ }
3598
+ ]
3599
+ }
3600
+ end
3601
+
3602
+ let(:correct_mapped_result) do
3603
+ period = correct_response_body['available_periods'][0]
3604
+
3605
+ [
3606
+ Cronofy::AvailablePeriod.new(
3607
+ available_period_id: period['available_period_id'],
3608
+ start: period['start'],
3609
+ end: period['end']
3610
+ )
3611
+ ]
3612
+ end
3613
+
3614
+ subject { client.get_available_periods }
3615
+
3616
+ it_behaves_like 'a Cronofy request'
3617
+ it_behaves_like 'a Cronofy request with mapped return value'
3618
+ end
3619
+
3620
+ context "filterd by date range" do
3621
+ let(:tzid) { "America/New_York" }
3622
+ let(:from) { "2020-07-01" }
3623
+ let(:to) { "2020-07-31" }
3624
+ let(:request_url) { "https://api.cronofy.com/v1/available_periods?from=#{from}&to=#{to}&tzid=#{tzid}" }
3625
+ let(:method) { :get }
3626
+
3627
+ let(:correct_response_code) { 200 }
3628
+ let(:correct_response_body) do
3629
+ {
3630
+ "available_periods" => [
3631
+ {
3632
+ "available_period_id" => "qTtZdczOccgaPncGJaCiLg",
3633
+ "start" => "2020-07-26T15:30:00Z",
3634
+ "end" => "2020-07-26T17:00:00Z"
3635
+ }
3636
+ ]
3637
+ }
3638
+ end
3639
+
3640
+ let(:correct_mapped_result) do
3641
+ period = correct_response_body['available_periods'][0]
3642
+
3643
+ [
3644
+ Cronofy::AvailablePeriod.new(
3645
+ available_period_id: period['available_period_id'],
3646
+ start: period['start'],
3647
+ end: period['end']
3648
+ )
3649
+ ]
3650
+ end
3651
+
3652
+ subject { client.get_available_periods(from: from, to: to, tzid: tzid) }
3653
+
3654
+ it_behaves_like 'a Cronofy request'
3655
+ it_behaves_like 'a Cronofy request with mapped return value'
3656
+ end
3657
+
3658
+ context "requesting localized times" do
3659
+ let(:tzid) { "America/New_York" }
3660
+ let(:localized_times) { true }
3661
+ let(:request_url) { "https://api.cronofy.com/v1/available_periods?tzid=#{tzid}&localized_times=true" }
3662
+ let(:method) { :get }
3663
+
3664
+ let(:correct_response_code) { 200 }
3665
+ let(:correct_response_body) do
3666
+ {
3667
+ "available_periods" => [
3668
+ {
3669
+ "available_period_id" => "qTtZdczOccgaPncGJaCiLg",
3670
+ "start" => "2020-07-26T15:30:00Z",
3671
+ "end" => "2020-07-26T17:00:00Z"
3672
+ }
3673
+ ]
3674
+ }
3675
+ end
3676
+
3677
+ let(:correct_mapped_result) do
3678
+ period = correct_response_body['available_periods'][0]
3679
+
3680
+ [
3681
+ Cronofy::AvailablePeriod.new(
3682
+ available_period_id: period['available_period_id'],
3683
+ start: period['start'],
3684
+ end: period['end']
3685
+ )
3686
+ ]
3687
+ end
3688
+
3689
+ subject { client.get_available_periods(tzid: tzid, localized_times: true) }
3690
+
3691
+ it_behaves_like 'a Cronofy request'
3692
+ it_behaves_like 'a Cronofy request with mapped return value'
3693
+ end
3694
+ end
3695
+
3696
+ describe '#delete_available_period' do
3697
+ let(:available_period_id) { 'default'}
3698
+ let(:request_url) { "https://api.cronofy.com/v1/available_periods" }
3699
+ let(:method) { :delete }
3700
+ let(:request_body) {
3701
+ { available_period_id: available_period_id}
3702
+ }
3703
+ let(:correct_response_code) { 202 }
3704
+ let(:correct_response_body) { "" }
3705
+ let(:correct_mapped_result) { nil }
3706
+
3707
+ subject { client.delete_available_period(available_period_id) }
3708
+
3709
+ it_behaves_like 'a Cronofy request'
3710
+ it_behaves_like 'a Cronofy request with mapped return value'
3711
+ end
3712
+ end