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.
- checksums.yaml +5 -5
- data/CHANGELOG.md +262 -0
- data/Gemfile +2 -0
- data/README.md +100 -46
- data/cronofy.gemspec +16 -13
- data/lib/cronofy/api_key.rb +73 -0
- data/lib/cronofy/auth.rb +149 -46
- data/lib/cronofy/client.rb +1928 -72
- data/lib/cronofy/errors.rb +68 -8
- data/lib/cronofy/response_parser.rb +36 -3
- data/lib/cronofy/time_encoding.rb +31 -0
- data/lib/cronofy/types.rb +420 -0
- data/lib/cronofy/version.rb +1 -1
- data/lib/cronofy.rb +68 -4
- data/spec/lib/cronofy/auth_spec.rb +559 -0
- data/spec/lib/cronofy/client_spec.rb +3712 -0
- data/spec/lib/cronofy/date_or_time_spec.rb +12 -0
- data/spec/lib/cronofy/errors_spec.rb +81 -0
- data/spec/lib/cronofy/event_spec.rb +121 -0
- data/spec/response_parser_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- metadata +73 -21
- data/.gitignore +0 -14
- data/.travis.yml +0 -16
- data/script/ci +0 -7
@@ -0,0 +1,559 @@
|
|
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
|
+
let(:linking_profile_hash) { nil }
|
18
|
+
let(:account_id) { nil }
|
19
|
+
|
20
|
+
before(:all) do
|
21
|
+
WebMock.reset!
|
22
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:api_token_url) { "https://api.cronofy.com/oauth/token" }
|
26
|
+
let(:app_token_url) { "https://app.cronofy.com/oauth/token" }
|
27
|
+
|
28
|
+
let(:response_status) { 200 }
|
29
|
+
|
30
|
+
before(:each) do
|
31
|
+
stub_request(:post, api_token_url)
|
32
|
+
.with(
|
33
|
+
body: {
|
34
|
+
client_id: client_id,
|
35
|
+
client_secret: client_secret,
|
36
|
+
grant_type: "refresh_token",
|
37
|
+
refresh_token: refresh_token,
|
38
|
+
},
|
39
|
+
headers: {
|
40
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
41
|
+
'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
|
42
|
+
}
|
43
|
+
)
|
44
|
+
.to_return(
|
45
|
+
status: response_status,
|
46
|
+
body: {
|
47
|
+
access_token: new_access_token,
|
48
|
+
token_type: 'bearer',
|
49
|
+
expires_in: expires_in,
|
50
|
+
refresh_token: new_refresh_token,
|
51
|
+
scope: scope,
|
52
|
+
}.to_json,
|
53
|
+
headers: {
|
54
|
+
"Content-Type" => "application/json; charset=utf-8"
|
55
|
+
}
|
56
|
+
)
|
57
|
+
|
58
|
+
stub_request(:post, app_token_url)
|
59
|
+
.with(
|
60
|
+
body: {
|
61
|
+
client_id: client_id,
|
62
|
+
client_secret: client_secret,
|
63
|
+
code: code,
|
64
|
+
grant_type: "authorization_code",
|
65
|
+
redirect_uri: redirect_uri,
|
66
|
+
},
|
67
|
+
headers: {
|
68
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
69
|
+
'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
|
70
|
+
}
|
71
|
+
)
|
72
|
+
.to_return(
|
73
|
+
status: response_status,
|
74
|
+
body: {
|
75
|
+
access_token: new_access_token,
|
76
|
+
token_type: 'bearer',
|
77
|
+
expires_in: expires_in,
|
78
|
+
refresh_token: new_refresh_token,
|
79
|
+
scope: scope,
|
80
|
+
account_id: account_id,
|
81
|
+
linking_profile: linking_profile_hash,
|
82
|
+
}.to_json,
|
83
|
+
headers: {
|
84
|
+
"Content-Type" => "application/json; charset=utf-8"
|
85
|
+
}
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#user_auth_link' do
|
90
|
+
let(:input_scope) { %w{read_events list_calendars create_event} }
|
91
|
+
let(:state) { nil }
|
92
|
+
let(:scheme) { 'https' }
|
93
|
+
let(:host) { 'app.cronofy.com' }
|
94
|
+
let(:path) { '/oauth/authorize' }
|
95
|
+
let(:data_center_override) { nil }
|
96
|
+
let(:default_params) do
|
97
|
+
{
|
98
|
+
'client_id' => client_id,
|
99
|
+
'redirect_uri' => redirect_uri,
|
100
|
+
'response_type' => 'code',
|
101
|
+
'scope' => scope,
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
let(:auth) do
|
106
|
+
Cronofy::Auth.new(
|
107
|
+
client_id: client_id,
|
108
|
+
client_secret: client_secret,
|
109
|
+
data_center: data_center_override,
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
subject do
|
114
|
+
url = auth.user_auth_link(redirect_uri, scope: input_scope, state: state)
|
115
|
+
URI.parse(url)
|
116
|
+
end
|
117
|
+
|
118
|
+
shared_examples 'a user auth link provider' do
|
119
|
+
it 'contains the correct scheme' do
|
120
|
+
expect(subject.scheme).to eq scheme
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'contains the correct host' do
|
124
|
+
expect(subject.host).to eq host
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'contains the correct path' do
|
128
|
+
expect(subject.path).to eq path
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'contains the correct query params' do
|
132
|
+
expect(Rack::Utils.parse_query(subject.query)).to eq params
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
context 'when no state' do
|
137
|
+
let(:state) { nil }
|
138
|
+
let(:params) { default_params }
|
139
|
+
|
140
|
+
it_behaves_like 'a user auth link provider'
|
141
|
+
end
|
142
|
+
|
143
|
+
context 'when state is passed' do
|
144
|
+
let(:state) { SecureRandom.hex }
|
145
|
+
let(:params) do
|
146
|
+
default_params.merge('state' => state)
|
147
|
+
end
|
148
|
+
|
149
|
+
it_behaves_like 'a user auth link provider'
|
150
|
+
end
|
151
|
+
|
152
|
+
context 'when scope is a string' do
|
153
|
+
let(:input_scope) { scope }
|
154
|
+
let(:params) { default_params }
|
155
|
+
|
156
|
+
it_behaves_like 'a user auth link provider'
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'when data center overridden' do
|
160
|
+
let(:data_center_override) { :de }
|
161
|
+
let(:host) { 'app-de.cronofy.com' }
|
162
|
+
let(:params) { default_params }
|
163
|
+
|
164
|
+
it_behaves_like 'a user auth link provider'
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
shared_examples 'an authorization request' do
|
169
|
+
context 'when succeeds' do
|
170
|
+
it 'returns a correct Credentials object' do
|
171
|
+
expect(subject.access_token).to eq new_access_token
|
172
|
+
expect(subject.expires_in).to eq expires_in
|
173
|
+
expect(subject.refresh_token).to eq new_refresh_token
|
174
|
+
expect(subject.scope).to eq scope
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
context 'when fails' do
|
179
|
+
context 'with 400' do
|
180
|
+
let(:response_status) { 400 }
|
181
|
+
|
182
|
+
it 'throws BadRequestError' do
|
183
|
+
expect{ subject }.to raise_error(Cronofy::BadRequestError)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context 'with unrecognized code' do
|
188
|
+
let(:response_status) { 418 }
|
189
|
+
|
190
|
+
it 'throws Unknown error' do
|
191
|
+
expect{ subject }.to raise_error(Cronofy::UnknownError)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
context "client_id and client_secret not set" do
|
196
|
+
let(:client_id) { " " }
|
197
|
+
let(:client_secret) { " " }
|
198
|
+
|
199
|
+
it "throws a credentials missing error" do
|
200
|
+
expect { subject }.to raise_error(Cronofy::CredentialsMissingError, "OAuth client_id and client_secret must be set")
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
describe '#get_token_from_code' do
|
207
|
+
let(:data_center_override) { nil }
|
208
|
+
|
209
|
+
subject do
|
210
|
+
Cronofy::Auth.new(
|
211
|
+
client_id: client_id,
|
212
|
+
client_secret: client_secret,
|
213
|
+
data_center: data_center_override,
|
214
|
+
).get_token_from_code(code, redirect_uri)
|
215
|
+
end
|
216
|
+
|
217
|
+
context "with account_id" do
|
218
|
+
let(:account_id) { "acc_0123456789abc" }
|
219
|
+
|
220
|
+
it 'exposes the account_id' do
|
221
|
+
expect(subject.account_id).to eq account_id
|
222
|
+
end
|
223
|
+
|
224
|
+
it "includes the account_id in its hash" do
|
225
|
+
expected = {
|
226
|
+
:access_token => subject.access_token,
|
227
|
+
:expires_at => subject.expires_at,
|
228
|
+
:expires_in => subject.expires_in,
|
229
|
+
:refresh_token => subject.refresh_token,
|
230
|
+
:scope => subject.scope,
|
231
|
+
:account_id => account_id,
|
232
|
+
}
|
233
|
+
|
234
|
+
expect(subject.to_h).to eq(expected)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
describe '#application_calendar' do
|
239
|
+
let(:data_center_override) { nil }
|
240
|
+
|
241
|
+
let(:application_calendar_id) { "my-application-calendar" }
|
242
|
+
let(:cronofy_application_calendar_id) { "apc_54475485743" }
|
243
|
+
|
244
|
+
let(:application_calendar_url) { "https://api.cronofy.com/v1/application_calendars" }
|
245
|
+
|
246
|
+
before do
|
247
|
+
stub_request(:post, application_calendar_url)
|
248
|
+
.with(
|
249
|
+
body: {
|
250
|
+
client_id: client_id,
|
251
|
+
client_secret: client_secret,
|
252
|
+
application_calendar_id: application_calendar_id,
|
253
|
+
},
|
254
|
+
)
|
255
|
+
.to_return(
|
256
|
+
status: response_status,
|
257
|
+
body: {
|
258
|
+
access_token: new_access_token,
|
259
|
+
token_type: 'bearer',
|
260
|
+
expires_in: expires_in,
|
261
|
+
refresh_token: new_refresh_token,
|
262
|
+
scope: scope,
|
263
|
+
application_calendar_id: application_calendar_id,
|
264
|
+
sub: cronofy_application_calendar_id,
|
265
|
+
}.to_json,
|
266
|
+
headers: {
|
267
|
+
"Content-Type" => "application/json; charset=utf-8"
|
268
|
+
}
|
269
|
+
)
|
270
|
+
end
|
271
|
+
|
272
|
+
subject do
|
273
|
+
Cronofy::Auth.new(
|
274
|
+
client_id: client_id,
|
275
|
+
client_secret: client_secret,
|
276
|
+
data_center: data_center_override,
|
277
|
+
).application_calendar(application_calendar_id)
|
278
|
+
end
|
279
|
+
|
280
|
+
it_behaves_like 'an authorization request'
|
281
|
+
end
|
282
|
+
|
283
|
+
context "with linking profile" do
|
284
|
+
let(:linking_profile_hash) do
|
285
|
+
{
|
286
|
+
provider_name: "google",
|
287
|
+
profile_id: "pro_VmrZnDitjScsAAAG",
|
288
|
+
profile_name: "bob@example.com",
|
289
|
+
}
|
290
|
+
end
|
291
|
+
|
292
|
+
it "exposes the linking profile" do
|
293
|
+
expected = Cronofy::Credentials::LinkingProfile.new(linking_profile_hash)
|
294
|
+
expect(subject.linking_profile).to eq expected
|
295
|
+
end
|
296
|
+
|
297
|
+
it "includes the linking profile in its hash" do
|
298
|
+
expected = {
|
299
|
+
:access_token => subject.access_token,
|
300
|
+
:expires_at => subject.expires_at,
|
301
|
+
:expires_in => subject.expires_in,
|
302
|
+
:refresh_token => subject.refresh_token,
|
303
|
+
:scope => subject.scope,
|
304
|
+
:linking_profile => linking_profile_hash,
|
305
|
+
}
|
306
|
+
|
307
|
+
expect(subject.to_h).to eq(expected)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
context "with data center overridden" do
|
312
|
+
let(:data_center_override) { :de }
|
313
|
+
let(:api_token_url) { "https://api-de.cronofy.com/oauth/token" }
|
314
|
+
let(:app_token_url) { "https://app-de.cronofy.com/oauth/token" }
|
315
|
+
|
316
|
+
it_behaves_like 'an authorization request'
|
317
|
+
end
|
318
|
+
|
319
|
+
it_behaves_like 'an authorization request'
|
320
|
+
end
|
321
|
+
|
322
|
+
describe '#refresh!' do
|
323
|
+
context "access_token and refresh_token present" do
|
324
|
+
subject do
|
325
|
+
Cronofy::Auth.new(
|
326
|
+
client_id: client_id,
|
327
|
+
client_secret: client_secret,
|
328
|
+
access_token: access_token,
|
329
|
+
refresh_token: refresh_token,
|
330
|
+
).refresh!
|
331
|
+
end
|
332
|
+
|
333
|
+
it_behaves_like 'an authorization request'
|
334
|
+
end
|
335
|
+
|
336
|
+
context "no refresh_token" do
|
337
|
+
subject do
|
338
|
+
Cronofy::Auth.new(
|
339
|
+
client_id: client_id,
|
340
|
+
client_secret: client_secret,
|
341
|
+
access_token: access_token,
|
342
|
+
).refresh!
|
343
|
+
end
|
344
|
+
|
345
|
+
it "raises a credentials missing error" do
|
346
|
+
expect { subject }.to raise_error(Cronofy::CredentialsMissingError)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context "no access_token or refresh_token" do
|
351
|
+
subject do
|
352
|
+
Cronofy::Auth.new(
|
353
|
+
client_id: client_id,
|
354
|
+
client_secret: client_secret,
|
355
|
+
).refresh!
|
356
|
+
end
|
357
|
+
|
358
|
+
it "raises a credentials missing error" do
|
359
|
+
expect { subject }.to raise_error(Cronofy::CredentialsMissingError)
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
context "only refresh_token" do
|
364
|
+
subject do
|
365
|
+
Cronofy::Auth.new(
|
366
|
+
client_id: client_id,
|
367
|
+
client_secret: client_secret,
|
368
|
+
refresh_token: refresh_token,
|
369
|
+
).refresh!
|
370
|
+
end
|
371
|
+
|
372
|
+
it_behaves_like 'an authorization request'
|
373
|
+
end
|
374
|
+
|
375
|
+
context "with data center overridden" do
|
376
|
+
subject do
|
377
|
+
Cronofy::Auth.new(
|
378
|
+
client_id: client_id,
|
379
|
+
client_secret: client_secret,
|
380
|
+
access_token: access_token,
|
381
|
+
refresh_token: refresh_token,
|
382
|
+
data_center: :de,
|
383
|
+
).refresh!
|
384
|
+
end
|
385
|
+
|
386
|
+
let(:api_token_url) { "https://api-de.cronofy.com/oauth/token" }
|
387
|
+
let(:app_token_url) { "https://app-de.cronofy.com/oauth/token" }
|
388
|
+
|
389
|
+
it_behaves_like 'an authorization request'
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
describe "#revoke!" do
|
394
|
+
shared_examples 'successful revocation' do
|
395
|
+
let!(:revocation_request) do
|
396
|
+
stub_request(:post, "https://api.cronofy.com/oauth/token/revoke")
|
397
|
+
.with(
|
398
|
+
body: {
|
399
|
+
client_id: client_id,
|
400
|
+
client_secret: client_secret,
|
401
|
+
token: revoke_token,
|
402
|
+
},
|
403
|
+
headers: {
|
404
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
405
|
+
'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
|
406
|
+
}
|
407
|
+
)
|
408
|
+
.to_return(
|
409
|
+
status: response_status,
|
410
|
+
)
|
411
|
+
end
|
412
|
+
|
413
|
+
before do
|
414
|
+
auth.revoke!
|
415
|
+
end
|
416
|
+
|
417
|
+
it "unsets the access token" do
|
418
|
+
expect(auth.access_token).to be_nil
|
419
|
+
end
|
420
|
+
|
421
|
+
it "makes the revocation request" do
|
422
|
+
expect(revocation_request).to have_been_requested
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
context "access_token and refresh_token present" do
|
427
|
+
let(:auth) do
|
428
|
+
Cronofy::Auth.new(
|
429
|
+
client_id: client_id,
|
430
|
+
client_secret: client_secret,
|
431
|
+
access_token: access_token,
|
432
|
+
refresh_token: refresh_token,
|
433
|
+
)
|
434
|
+
end
|
435
|
+
|
436
|
+
let(:revoke_token) { refresh_token }
|
437
|
+
|
438
|
+
it_behaves_like 'successful revocation'
|
439
|
+
end
|
440
|
+
|
441
|
+
context "only refresh_token" do
|
442
|
+
let(:auth) do
|
443
|
+
Cronofy::Auth.new(
|
444
|
+
client_id: client_id,
|
445
|
+
client_secret: client_secret,
|
446
|
+
refresh_token: refresh_token,
|
447
|
+
)
|
448
|
+
end
|
449
|
+
|
450
|
+
let(:revoke_token) { refresh_token }
|
451
|
+
|
452
|
+
it_behaves_like 'successful revocation'
|
453
|
+
end
|
454
|
+
|
455
|
+
context "only access_token" do
|
456
|
+
let(:auth) do
|
457
|
+
Cronofy::Auth.new(
|
458
|
+
client_id: client_id,
|
459
|
+
client_secret: client_secret,
|
460
|
+
access_token: access_token,
|
461
|
+
)
|
462
|
+
end
|
463
|
+
|
464
|
+
let(:revoke_token) { access_token }
|
465
|
+
|
466
|
+
it_behaves_like 'successful revocation'
|
467
|
+
end
|
468
|
+
|
469
|
+
context "no access_token or refresh_token" do
|
470
|
+
let(:auth) do
|
471
|
+
Cronofy::Auth.new(client_id: client_id, client_secret: client_secret)
|
472
|
+
end
|
473
|
+
|
474
|
+
it "raises a credentials missing error" do
|
475
|
+
expect { auth.revoke! }.to raise_error(Cronofy::CredentialsMissingError)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
describe "#revoke_by_sub" do
|
481
|
+
let(:auth) do
|
482
|
+
Cronofy::Auth.new(
|
483
|
+
client_id: client_id,
|
484
|
+
client_secret: client_secret,
|
485
|
+
access_token: access_token,
|
486
|
+
refresh_token: refresh_token,
|
487
|
+
)
|
488
|
+
end
|
489
|
+
|
490
|
+
let!(:revocation_request) do
|
491
|
+
stub_request(:post, "https://api.cronofy.com/oauth/token/revoke")
|
492
|
+
.with(
|
493
|
+
body: {
|
494
|
+
client_id: client_id,
|
495
|
+
client_secret: client_secret,
|
496
|
+
sub: sub,
|
497
|
+
},
|
498
|
+
headers: {
|
499
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
500
|
+
'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
|
501
|
+
}
|
502
|
+
).to_return(status: response_status)
|
503
|
+
end
|
504
|
+
|
505
|
+
let(:sub) { "random_sub_value" }
|
506
|
+
|
507
|
+
before do
|
508
|
+
auth.revoke_by_sub(sub)
|
509
|
+
end
|
510
|
+
|
511
|
+
it "does not unset the access token for the current auth" do
|
512
|
+
expect(auth.access_token).not_to be_nil
|
513
|
+
end
|
514
|
+
|
515
|
+
it "makes the revocation request" do
|
516
|
+
expect(revocation_request).to have_been_requested
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
describe "#revoke_by_token" do
|
521
|
+
let(:auth) do
|
522
|
+
Cronofy::Auth.new(
|
523
|
+
client_id: client_id,
|
524
|
+
client_secret: client_secret,
|
525
|
+
access_token: access_token,
|
526
|
+
refresh_token: refresh_token,
|
527
|
+
)
|
528
|
+
end
|
529
|
+
|
530
|
+
let!(:revocation_request) do
|
531
|
+
stub_request(:post, "https://api.cronofy.com/oauth/token/revoke")
|
532
|
+
.with(
|
533
|
+
body: {
|
534
|
+
client_id: client_id,
|
535
|
+
client_secret: client_secret,
|
536
|
+
token: token,
|
537
|
+
},
|
538
|
+
headers: {
|
539
|
+
'Content-Type' => 'application/x-www-form-urlencoded',
|
540
|
+
'User-Agent' => "Cronofy Ruby #{Cronofy::VERSION}",
|
541
|
+
}
|
542
|
+
).to_return(status: response_status)
|
543
|
+
end
|
544
|
+
|
545
|
+
let(:token) { "random_token_value" }
|
546
|
+
|
547
|
+
before do
|
548
|
+
auth.revoke_by_token(token)
|
549
|
+
end
|
550
|
+
|
551
|
+
it "does not unset the access token for the current auth" do
|
552
|
+
expect(auth.access_token).not_to be_nil
|
553
|
+
end
|
554
|
+
|
555
|
+
it "makes the revocation request" do
|
556
|
+
expect(revocation_request).to have_been_requested
|
557
|
+
end
|
558
|
+
end
|
559
|
+
end
|