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.
- checksums.yaml +4 -4
- data/README.md +59 -31
- data/cronofy.gemspec +15 -12
- data/lib/cronofy.rb +1 -2
- data/lib/cronofy/auth.rb +26 -42
- data/lib/cronofy/client.rb +394 -74
- data/lib/cronofy/errors.rb +31 -10
- data/lib/cronofy/response_parser.rb +28 -3
- data/lib/cronofy/types.rb +143 -0
- data/lib/cronofy/version.rb +1 -1
- data/spec/lib/cronofy/auth_spec.rb +123 -0
- data/spec/lib/cronofy/client_spec.rb +519 -0
- data/spec/response_parser_spec.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- metadata +55 -23
- data/.gitignore +0 -14
- data/.travis.yml +0 -16
- data/script/ci +0 -7
data/lib/cronofy/errors.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
10
|
-
|
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
|
data/lib/cronofy/version.rb
CHANGED
@@ -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
|