cronofy 0.0.5 → 0.1.0

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.
@@ -1,14 +1,11 @@
1
1
  module Cronofy
2
2
  class CronofyError < StandardError
3
-
4
3
  end
5
4
 
6
5
  class CredentialsMissingError < CronofyError
7
-
8
6
  def initialize(message=nil)
9
7
  super(message || "No credentials supplied")
10
8
  end
11
-
12
9
  end
13
10
 
14
11
  class APIError < CronofyError
@@ -32,28 +29,52 @@ module Cronofy
32
29
  end
33
30
  end
34
31
 
35
- class NotFoundError < APIError
32
+ class BadRequestError < APIError
33
+ end
36
34
 
35
+ class NotFoundError < APIError
37
36
  end
38
37
 
39
38
  class AuthenticationFailureError < APIError
40
-
41
39
  end
42
40
 
43
41
  class AuthorizationFailureError < APIError
44
-
45
42
  end
46
43
 
47
44
  class InvalidRequestError < APIError
48
-
49
45
  end
50
46
 
51
47
  class TooManyRequestsError < APIError
52
-
53
48
  end
54
49
 
55
50
  class UnknownError < APIError
56
-
57
51
  end
58
52
 
59
- end
53
+ # Internal: Helper methods for raising more meaningful errors.
54
+ class Errors
55
+ ERROR_MAP = {
56
+ 400 => BadRequestError,
57
+ 401 => AuthenticationFailureError,
58
+ 403 => AuthorizationFailureError,
59
+ 404 => NotFoundError,
60
+ 422 => InvalidRequestError,
61
+ 429 => TooManyRequestsError,
62
+ }.freeze
63
+
64
+ def self.map_error(error)
65
+ raise_error(error.response)
66
+ end
67
+
68
+ def self.raise_if_error(response)
69
+ return if response.status == 200
70
+ raise_error(response)
71
+ end
72
+
73
+ private
74
+
75
+ def self.raise_error(response)
76
+ error_class = ERROR_MAP.fetch(response.status, UnknownError)
77
+ raise error_class.new(response.headers['status'], response)
78
+ end
79
+ end
80
+ end
@@ -1,13 +1,38 @@
1
1
  require 'json'
2
2
 
3
3
  module Cronofy
4
+ # Internal: Class for dealing with the parsing of API responses.
4
5
  class ResponseParser
5
6
  def initialize(response)
6
7
  @response = response
7
8
  end
8
9
 
9
- def parse_json
10
- JSON.parse @response.body
10
+ def parse_collection(type, attribute = nil)
11
+ target = parsing_target(attribute)
12
+ target.map { |item| type.new(item) }
13
+ end
14
+
15
+ def parse_json(type, attribute = nil)
16
+ target = parsing_target(attribute)
17
+ type.new(target)
18
+ end
19
+
20
+ def json
21
+ json_hash.dup
22
+ end
23
+
24
+ private
25
+
26
+ def json_hash
27
+ @json_hash ||= JSON.parse(@response.body)
28
+ end
29
+
30
+ def parsing_target(attribute)
31
+ if attribute
32
+ json_hash[attribute]
33
+ else
34
+ json_hash
35
+ end
11
36
  end
12
37
  end
13
- end
38
+ end
@@ -0,0 +1,143 @@
1
+ require "date"
2
+ require "hashie"
3
+
4
+ module Cronofy
5
+ class Credentials
6
+ attr_reader :access_token
7
+ attr_reader :expires_at
8
+ attr_reader :expires_in
9
+ attr_reader :refresh_token
10
+ attr_reader :scope
11
+
12
+ def initialize(oauth_token)
13
+ @access_token = oauth_token.token
14
+ @expires_at = oauth_token.expires_at
15
+ @expires_in = oauth_token.expires_in
16
+ @refresh_token = oauth_token.refresh_token
17
+ @scope = oauth_token.params['scope']
18
+ end
19
+
20
+ def to_hash
21
+ {
22
+ access_token: access_token,
23
+ expires_at: expires_at,
24
+ expires_in: expires_in,
25
+ refresh_token: refresh_token,
26
+ scope: scope,
27
+ }
28
+ end
29
+ end
30
+
31
+ class DateOrTime
32
+ def initialize(args)
33
+ # Prefer time if both provided as it is more accurate
34
+ if args[:time]
35
+ @time = args[:time]
36
+ else
37
+ @date = args[:date]
38
+ end
39
+ end
40
+
41
+ def self.coerce(value)
42
+ begin
43
+ time = Time.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
44
+ rescue
45
+ begin
46
+ date = Date.strptime(value, '%Y-%m-%d')
47
+ rescue
48
+ end
49
+ end
50
+
51
+ coerced = self.new(time: time, date: date)
52
+
53
+ raise "Failed to coerce \"#{value}\"" unless coerced.time? or coerced.date?
54
+
55
+ coerced
56
+ end
57
+
58
+ def date
59
+ @date
60
+ end
61
+
62
+ def date?
63
+ !!@date
64
+ end
65
+
66
+ def time
67
+ @time
68
+ end
69
+
70
+ def time?
71
+ !!@time
72
+ end
73
+
74
+ def to_date
75
+ if date?
76
+ date
77
+ else
78
+ time.to_date
79
+ end
80
+ end
81
+
82
+ def to_time
83
+ if time?
84
+ time
85
+ else
86
+ # Convert dates to UTC time, not local time
87
+ Time.utc(date.year, date.month, date.day)
88
+ end
89
+ end
90
+
91
+ def ==(other)
92
+ case other
93
+ when DateOrTime
94
+ if self.time?
95
+ other.time? and self.time == other.time
96
+ elsif self.date?
97
+ other.date? and self.date == other.date
98
+ else
99
+ # Both neither date nor time
100
+ self.time? == other.time? and self.date? == other.date?
101
+ end
102
+ else
103
+ false
104
+ end
105
+ end
106
+
107
+ def inspect
108
+ to_s
109
+ end
110
+
111
+ def to_s
112
+ if time?
113
+ "<#{self.class} time=#{self.time}>"
114
+ elsif date?
115
+ "<#{self.class} date=#{self.date}>"
116
+ else
117
+ "<#{self.class} empty>"
118
+ end
119
+ end
120
+ end
121
+
122
+ class Account < Hashie::Mash
123
+ end
124
+
125
+ class Calendar < Hashie::Mash
126
+ end
127
+
128
+ class Channel < Hashie::Mash
129
+ end
130
+
131
+ class Event < Hashie::Mash
132
+ include Hashie::Extensions::Coercion
133
+
134
+ coerce_key :start, DateOrTime
135
+ coerce_key :end, DateOrTime
136
+ end
137
+
138
+ class PagedEventsResult < Hashie::Mash
139
+ include Hashie::Extensions::Coercion
140
+
141
+ coerce_key :events, Array[Event]
142
+ end
143
+ end
@@ -1,3 +1,3 @@
1
1
  module Cronofy
2
- VERSION = "0.0.5"
2
+ VERSION = "0.1.0".freeze
3
3
  end
@@ -0,0 +1,123 @@
1
+ require_relative '../../spec_helper'
2
+
3
+ describe Cronofy::Auth do
4
+ let(:client_id) { 'client_id_123' }
5
+ let(:client_secret) { 'client_secret_456' }
6
+
7
+ let(:code) { 'code_789' }
8
+ let(:redirect_uri) { 'http://red.ire.ct/Uri' }
9
+ let(:access_token) { 'access_token_123' }
10
+ let(:refresh_token) { 'refresh_token_456' }
11
+
12
+ let(:new_access_token) { "new_access_token_2342" }
13
+ let(:new_refresh_token) { "new_refresh_token_7898" }
14
+ let(:expires_in) { 10000 }
15
+ let(:scope) { 'read_events list_calendars create_event' }
16
+
17
+ before(:all) do
18
+ WebMock.reset!
19
+ WebMock.disable_net_connect!(allow_localhost: true)
20
+ end
21
+
22
+ let(:response_status) { 200 }
23
+
24
+ before(:each) do
25
+ stub_request(:post, "https://api.cronofy.com/oauth/token")
26
+ .with(
27
+ body: {
28
+ client_id: client_id,
29
+ client_secret: client_secret,
30
+ grant_type: "refresh_token",
31
+ refresh_token: refresh_token,
32
+ },
33
+ headers: {
34
+ 'Content-Type' => 'application/x-www-form-urlencoded',
35
+ 'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
36
+ }
37
+ )
38
+ .to_return(
39
+ status: response_status,
40
+ body: {
41
+ access_token: new_access_token,
42
+ token_type: 'bearer',
43
+ expires_in: expires_in,
44
+ refresh_token: new_refresh_token,
45
+ scope: scope,
46
+ }.to_json,
47
+ headers: {
48
+ "Content-Type" => "application/json; charset=utf-8"
49
+ }
50
+ )
51
+
52
+ stub_request(:post, "https://app.cronofy.com/oauth/token")
53
+ .with(
54
+ body: {
55
+ client_id: client_id,
56
+ client_secret: client_secret,
57
+ code: code,
58
+ grant_type: "authorization_code",
59
+ redirect_uri: redirect_uri,
60
+ },
61
+ headers: {
62
+ 'Content-Type' => 'application/x-www-form-urlencoded',
63
+ 'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
64
+ }
65
+ )
66
+ .to_return(
67
+ status: response_status,
68
+ body: {
69
+ access_token: new_access_token,
70
+ token_type: 'bearer',
71
+ expires_in: expires_in,
72
+ refresh_token: new_refresh_token,
73
+ scope: scope,
74
+ }.to_json,
75
+ headers: {
76
+ "Content-Type" => "application/json; charset=utf-8"
77
+ }
78
+ )
79
+ end
80
+
81
+ shared_examples 'an authorization request' do
82
+ context 'when succeeds' do
83
+ it 'returns a correct Credentials object' do
84
+ expect(subject.access_token).to eq new_access_token
85
+ expect(subject.expires_in).to eq expires_in
86
+ expect(subject.refresh_token).to eq new_refresh_token
87
+ expect(subject.scope).to eq scope
88
+ end
89
+ end
90
+
91
+ context 'when fails' do
92
+ context 'with 400' do
93
+ let(:response_status) { 400 }
94
+
95
+ it 'throws BadRequestError' do
96
+ expect{ subject }.to raise_error(Cronofy::BadRequestError)
97
+ end
98
+ end
99
+
100
+ context 'with unrecognized code' do
101
+ let(:response_status) { 418 }
102
+
103
+ it 'throws Unknown error' do
104
+ expect{ subject }.to raise_error(Cronofy::UnknownError)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '#get_token_from_code' do
111
+ subject { Cronofy::Auth.new(client_id, client_secret).get_token_from_code(code, redirect_uri) }
112
+
113
+ it_behaves_like 'an authorization request'
114
+ end
115
+
116
+ describe '#refresh!' do
117
+ subject do
118
+ Cronofy::Auth.new(client_id, client_secret, access_token, refresh_token).refresh!
119
+ end
120
+
121
+ it_behaves_like 'an authorization request'
122
+ end
123
+ end
@@ -0,0 +1,519 @@
1
+
2
+ require_relative '../../spec_helper'
3
+
4
+ describe Cronofy::Client do
5
+ before(:all) do
6
+ WebMock.reset!
7
+ WebMock.disable_net_connect!(allow_localhost: true)
8
+ end
9
+
10
+ let(:token) { 'token_123' }
11
+ let(:base_request_headers) do
12
+ {
13
+ "Authorization" => "Bearer #{token}",
14
+ "User-Agent" => "Cronofy Ruby #{::Cronofy::VERSION}",
15
+ }
16
+ end
17
+
18
+ let(:json_request_headers) do
19
+ base_request_headers.merge("Content-Type" => "application/json; charset=utf-8")
20
+ end
21
+
22
+ let(:request_headers) do
23
+ base_request_headers
24
+ end
25
+
26
+ let(:request_body) { nil }
27
+
28
+ let(:client) do
29
+ Cronofy::Client.new(
30
+ client_id: 'client_id_123',
31
+ client_secret: 'client_secret_456',
32
+ access_token: token,
33
+ refresh_token: 'refresh_token_456',
34
+ )
35
+ end
36
+
37
+ let(:correct_response_headers) do
38
+ { 'Content-Type' => 'application/json; charset=utf-8' }
39
+ end
40
+
41
+ shared_examples 'a Cronofy request with mapped return value' do
42
+ it 'returns the correct response when no error' do
43
+ stub_request(method, request_url)
44
+ .with(headers: request_headers,
45
+ body: request_body)
46
+ .to_return(status: correct_response_code,
47
+ headers: correct_response_headers,
48
+ body: correct_response_body.to_json)
49
+
50
+ expect(subject).to eq correct_mapped_result
51
+ end
52
+ end
53
+
54
+ shared_examples 'a Cronofy request' do
55
+ it "doesn't raise an error when response is correct" do
56
+ stub_request(method, request_url)
57
+ .with(headers: request_headers,
58
+ body: request_body)
59
+ .to_return(status: correct_response_code,
60
+ headers: correct_response_headers,
61
+ body: correct_response_body.to_json)
62
+
63
+ expect{ subject }.not_to raise_error
64
+ end
65
+
66
+ it 'raises AuthenticationFailureError on 401s' do
67
+ stub_request(method, request_url)
68
+ .with(headers: request_headers,
69
+ body: request_body)
70
+ .to_return(status: 401,
71
+ headers: correct_response_headers,
72
+ body: correct_response_body.to_json)
73
+ expect{ subject }.to raise_error(Cronofy::AuthenticationFailureError)
74
+ end
75
+
76
+ it 'raises AuthorizationFailureError on 403s' do
77
+ stub_request(method, request_url)
78
+ .with(headers: request_headers,
79
+ body: request_body)
80
+ .to_return(status: 403,
81
+ headers: correct_response_headers,
82
+ body: correct_response_body.to_json)
83
+ expect{ subject }.to raise_error(Cronofy::AuthorizationFailureError)
84
+ end
85
+
86
+ it 'raises NotFoundError on 404s' do
87
+ stub_request(method, request_url)
88
+ .with(headers: request_headers,
89
+ body: request_body)
90
+ .to_return(status: 404,
91
+ headers: correct_response_headers,
92
+ body: correct_response_body.to_json)
93
+ expect{ subject }.to raise_error(::Cronofy::NotFoundError)
94
+ end
95
+
96
+ it 'raises InvalidRequestError on 422s' do
97
+ stub_request(method, request_url)
98
+ .with(headers: request_headers,
99
+ body: request_body)
100
+ .to_return(status: 422,
101
+ headers: correct_response_headers,
102
+ body: correct_response_body.to_json)
103
+ expect{ subject }.to raise_error(::Cronofy::InvalidRequestError)
104
+ end
105
+
106
+ it 'raises AuthenticationFailureError on 401s' do
107
+ stub_request(method, request_url)
108
+ .with(headers: request_headers,
109
+ body: request_body)
110
+ .to_return(status: 429,
111
+ headers: correct_response_headers,
112
+ body: correct_response_body.to_json)
113
+ expect{ subject }.to raise_error(::Cronofy::TooManyRequestsError)
114
+ end
115
+ end
116
+
117
+ describe '#list_calendars' do
118
+ let(:request_url) { 'https://api.cronofy.com/v1/calendars' }
119
+ let(:method) { :get }
120
+ let(:correct_response_code) { 200 }
121
+ let(:correct_response_body) do
122
+ {
123
+ "calendars" => [
124
+ {
125
+ "provider_name" => "google",
126
+ "profile_name" => "example@cronofy.com",
127
+ "calendar_id" => "cal_n23kjnwrw2_jsdfjksn234",
128
+ "calendar_name" => "Home",
129
+ "calendar_readonly" => false,
130
+ "calendar_deleted" => false
131
+ },
132
+ {
133
+ "provider_name" => "google",
134
+ "profile_name" => "example@cronofy.com",
135
+ "calendar_id" => "cal_n23kjnwrw2_n1k323nkj23",
136
+ "calendar_name" => "Work",
137
+ "calendar_readonly" => true,
138
+ "calendar_deleted" => true
139
+ },
140
+ {
141
+ "provider_name" => "apple",
142
+ "profile_name" => "example@cronofy.com",
143
+ "calendar_id" => "cal_n23kjnwrw2_3nkj23wejk1",
144
+ "calendar_name" => "Bank Holidays",
145
+ "calendar_readonly" => true,
146
+ "calendar_deleted" => false
147
+ }
148
+ ]
149
+ }
150
+ end
151
+
152
+ let(:correct_mapped_result) do
153
+ correct_response_body["calendars"].map { |cal| Cronofy::Calendar.new(cal) }
154
+ end
155
+
156
+ subject { client.list_calendars }
157
+
158
+ it_behaves_like 'a Cronofy request'
159
+ it_behaves_like 'a Cronofy request with mapped return value'
160
+ end
161
+
162
+ describe 'Events' do
163
+ describe '#create_or_update_event' do
164
+ let(:calendar_id) { 'calendar_id_123'}
165
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events" }
166
+ let(:method) { :post }
167
+ let(:request_headers) { json_request_headers }
168
+ let(:event) do
169
+ {
170
+ :event_id => "qTtZdczOccgaPncGJaCiLg",
171
+ :summary => "Board meeting",
172
+ :description => "Discuss plans for the next quarter.",
173
+ :start => start_datetime,
174
+ :end => end_datetime,
175
+ :location => {
176
+ :description => "Board room"
177
+ }
178
+ }
179
+ end
180
+ let(:request_body) do
181
+ hash_including(:event_id => "qTtZdczOccgaPncGJaCiLg",
182
+ :summary => "Board meeting",
183
+ :description => "Discuss plans for the next quarter.",
184
+ :start => start_datetime_string,
185
+ :end => end_datetime_string,
186
+ :location => {
187
+ :description => "Board room"
188
+ })
189
+ end
190
+ let(:correct_response_code) { 202 }
191
+ let(:correct_response_body) { nil }
192
+
193
+ subject { client.create_or_update_event(calendar_id, event) }
194
+
195
+ context 'when start/end are Times' do
196
+ let(:start_datetime) { Time.utc(2014, 8, 5, 15, 30, 0) }
197
+ let(:end_datetime) { Time.utc(2014, 8, 5, 17, 0, 0) }
198
+ let(:start_datetime_string) { "2014-08-05T15:30:00Z" }
199
+ let(:end_datetime_string) { "2014-08-05T17:00:00Z" }
200
+
201
+ it_behaves_like 'a Cronofy request'
202
+ end
203
+ end
204
+
205
+ describe '#read_events' do
206
+ before do
207
+ stub_request(method, request_url)
208
+ .with(headers: request_headers,
209
+ body: request_body)
210
+ .to_return(status: correct_response_code,
211
+ headers: correct_response_headers,
212
+ body: correct_response_body.to_json)
213
+
214
+ stub_request(:get, next_page_url)
215
+ .with(headers: request_headers)
216
+ .to_return(status: correct_response_code,
217
+ headers: correct_response_headers,
218
+ body: next_page_body.to_json)
219
+ end
220
+
221
+
222
+ let(:request_url_prefix) { 'https://api.cronofy.com/v1/events' }
223
+ let(:method) { :get }
224
+ let(:correct_response_code) { 200 }
225
+ let(:next_page_url) do
226
+ "https://next.page.com/08a07b034306679e"
227
+ end
228
+
229
+ let(:params) { Hash.new }
230
+ let(:request_url) { request_url_prefix + "?tzid=Etc/UTC" }
231
+
232
+ let(:correct_response_body) do
233
+ {
234
+ 'pages' => {
235
+ 'current' => 1,
236
+ 'total' => 2,
237
+ 'next_page' => next_page_url
238
+ },
239
+ 'events' => [
240
+ {
241
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
242
+ 'event_uid' => 'evt_external_54008b1a4a41730f8d5c6037',
243
+ 'summary' => 'Company Retreat',
244
+ 'description' => '',
245
+ 'start' => '2014-09-06',
246
+ 'end' => '2014-09-08',
247
+ 'deleted' => false
248
+ },
249
+ {
250
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
251
+ 'event_uid' => 'evt_external_54008b1a4a41730f8d5c6038',
252
+ 'summary' => 'Dinner with Laura',
253
+ 'description' => '',
254
+ 'start' => '2014-09-13T19:00:00Z',
255
+ 'end' => '2014-09-13T21:00:00Z',
256
+ 'deleted' => false,
257
+ 'location' => {
258
+ 'description' => 'Pizzeria'
259
+ }
260
+ }
261
+ ]
262
+ }
263
+ end
264
+
265
+ let(:next_page_body) do
266
+ {
267
+ 'pages' => {
268
+ 'current' => 2,
269
+ 'total' => 2,
270
+ },
271
+ 'events' => [
272
+ {
273
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
274
+ 'event_uid' => 'evt_external_54008b1a4a4173023402934d',
275
+ 'summary' => 'Company Retreat Extended',
276
+ 'description' => '',
277
+ 'start' => '2014-09-06',
278
+ 'end' => '2014-09-08',
279
+ 'deleted' => false
280
+ },
281
+ {
282
+ 'calendar_id' => 'cal_U9uuErStTG@EAAAB_IsAsykA2DBTWqQTf-f0kJw',
283
+ 'event_uid' => 'evt_external_54008b1a4a41198273921312',
284
+ 'summary' => 'Dinner with Paul',
285
+ 'description' => '',
286
+ 'start' => '2014-09-13T19:00:00Z',
287
+ 'end' => '2014-09-13T21:00:00Z',
288
+ 'deleted' => false,
289
+ 'location' => {
290
+ 'description' => 'Cafe'
291
+ }
292
+ }
293
+ ]
294
+ }
295
+ end
296
+
297
+ let(:correct_mapped_result) do
298
+ first_page_events = correct_response_body['events'].map { |event| Cronofy::Event.new(event) }
299
+ second_page_events = next_page_body['events'].map { |event| Cronofy::Event.new(event) }
300
+
301
+ first_page_events + second_page_events
302
+ end
303
+
304
+ subject do
305
+ # By default force evaluation
306
+ client.read_events(params).to_a
307
+ end
308
+
309
+ context 'when all params are passed' do
310
+ let(:params) do
311
+ {
312
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
313
+ to: Time.new(2014, 10, 1, 0, 0, 1, '+00:00'),
314
+ tzid: 'Etc/UTC',
315
+ include_deleted: false,
316
+ include_moved: true,
317
+ last_modified: Time.new(2014, 8, 1, 0, 0, 1, '+00:00')
318
+ }
319
+ end
320
+ let(:request_url) do
321
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
322
+ "&to=2014-10-01T00:00:01Z&tzid=Etc/UTC&include_deleted=false" \
323
+ "&include_moved=true&last_modified=2014-08-01T00:00:01Z"
324
+ end
325
+
326
+ it_behaves_like 'a Cronofy request'
327
+ it_behaves_like 'a Cronofy request with mapped return value'
328
+ end
329
+
330
+ context 'when some params are passed' do
331
+ let(:params) do
332
+ {
333
+ from: Time.new(2014, 9, 1, 0, 0, 1, '+00:00'),
334
+ include_deleted: false,
335
+ }
336
+ end
337
+ let(:request_url) do
338
+ "#{request_url_prefix}?from=2014-09-01T00:00:01Z" \
339
+ "&tzid=Etc/UTC&include_deleted=false"
340
+ end
341
+
342
+ it_behaves_like 'a Cronofy request'
343
+ it_behaves_like 'a Cronofy request with mapped return value'
344
+ end
345
+
346
+ context "when unknown flags are passed" do
347
+ let(:params) do
348
+ {
349
+ unknown_bool: true,
350
+ unknown_number: 5,
351
+ unknown_string: "foo-bar-baz",
352
+ }
353
+ end
354
+
355
+ let(:request_url) do
356
+ "#{request_url_prefix}?tzid=Etc/UTC" \
357
+ "&unknown_bool=true" \
358
+ "&unknown_number=5" \
359
+ "&unknown_string=foo-bar-baz"
360
+ end
361
+
362
+ it_behaves_like 'a Cronofy request'
363
+ it_behaves_like 'a Cronofy request with mapped return value'
364
+ end
365
+
366
+ context "next page not found" do
367
+ before do
368
+ stub_request(:get, next_page_url)
369
+ .with(headers: request_headers)
370
+ .to_return(status: 404,
371
+ headers: correct_response_headers)
372
+ end
373
+
374
+ it "raises an error" do
375
+ expect{ subject }.to raise_error(::Cronofy::NotFoundError)
376
+ end
377
+ end
378
+
379
+ context "only first event" do
380
+ before do
381
+ # Ensure an error if second page is requested
382
+ stub_request(:get, next_page_url)
383
+ .with(headers: request_headers)
384
+ .to_return(status: 404,
385
+ headers: correct_response_headers)
386
+ end
387
+
388
+ let(:first_event) do
389
+ Cronofy::Event.new(correct_response_body["events"].first)
390
+ end
391
+
392
+ subject do
393
+ client.read_events(params).first
394
+ end
395
+
396
+ it "returns the first event from the first page" do
397
+ expect(subject).to eq(first_event)
398
+ end
399
+ end
400
+ end
401
+
402
+ describe '#delete_event' do
403
+ let(:calendar_id) { 'calendar_id_123'}
404
+ let(:request_url) { "https://api.cronofy.com/v1/calendars/#{calendar_id}/events" }
405
+ let(:event_id) { 'event_id_456' }
406
+ let(:method) { :delete }
407
+ let(:request_headers) { json_request_headers }
408
+ let(:request_body) { { :event_id => event_id } }
409
+ let(:correct_response_code) { 202 }
410
+ let(:correct_response_body) { nil }
411
+
412
+ subject { client.delete_event(calendar_id, event_id) }
413
+
414
+ it_behaves_like 'a Cronofy request'
415
+ end
416
+ end
417
+
418
+ describe 'Channels' do
419
+ let(:request_url) { 'https://api.cronofy.com/v1/channels' }
420
+
421
+ describe '#create_channel' do
422
+ let(:method) { :post }
423
+ let(:callback_url) { 'http://call.back/url' }
424
+ let(:request_headers) { json_request_headers }
425
+ let(:request_body) { hash_including(:callback_url => callback_url) }
426
+
427
+ let(:correct_response_code) { 200 }
428
+ let(:correct_response_body) do
429
+ {
430
+ 'channel' => {
431
+ 'channel_id' => 'channel_id_123',
432
+ 'callback_url' => ENV['CALLBACK_URL'],
433
+ 'filters' => {}
434
+ }
435
+ }
436
+ end
437
+
438
+ let(:correct_mapped_result) do
439
+ Cronofy::Channel.new(correct_response_body["channel"])
440
+ end
441
+
442
+ subject { client.create_channel(callback_url) }
443
+
444
+ it_behaves_like 'a Cronofy request'
445
+ it_behaves_like 'a Cronofy request with mapped return value'
446
+ end
447
+
448
+ describe '#list_channels' do
449
+ let(:method) { :get }
450
+
451
+ let(:correct_response_code) { 200 }
452
+ let(:correct_response_body) do
453
+ {
454
+ 'channels' => [
455
+ {
456
+ 'channel_id' => 'channel_id_123',
457
+ 'callback_url' => 'http://call.back/url',
458
+ 'filters' => {}
459
+ },
460
+ {
461
+ 'channel_id' => 'channel_id_456',
462
+ 'callback_url' => 'http://call.back/url2',
463
+ 'filters' => {}
464
+ }
465
+ ]
466
+ }
467
+ end
468
+
469
+ let(:correct_mapped_result) do
470
+ correct_response_body["channels"].map { |ch| Cronofy::Channel.new(ch) }
471
+ end
472
+
473
+ subject { client.list_channels }
474
+
475
+ it_behaves_like 'a Cronofy request'
476
+ it_behaves_like 'a Cronofy request with mapped return value'
477
+ end
478
+
479
+ describe '#close_channel' do
480
+ let(:channel_id) { "chn_1234567890" }
481
+ let(:method) { :delete }
482
+ let(:request_url) { "https://api.cronofy.com/v1/channels/#{channel_id}" }
483
+
484
+ let(:correct_response_code) { 202 }
485
+ let(:correct_response_body) { nil }
486
+
487
+ subject { client.close_channel(channel_id) }
488
+
489
+ it_behaves_like 'a Cronofy request'
490
+ end
491
+ end
492
+
493
+ describe "Account" do
494
+ let(:request_url) { "https://api.cronofy.com/v1/account" }
495
+
496
+ describe "#account" do
497
+ let(:method) { :get }
498
+
499
+ let(:correct_response_code) { 200 }
500
+ let(:correct_response_body) do
501
+ {
502
+ "account" => {
503
+ "account_id" => "acc_id_123",
504
+ "email" => "foo@example.com",
505
+ }
506
+ }
507
+ end
508
+
509
+ let(:correct_mapped_result) do
510
+ Cronofy::Account.new(correct_response_body["account"])
511
+ end
512
+
513
+ subject { client.account }
514
+
515
+ it_behaves_like "a Cronofy request"
516
+ it_behaves_like "a Cronofy request with mapped return value"
517
+ end
518
+ end
519
+ end