cronofy 0.0.5 → 0.37.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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