edh 0.1
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.
- data/.autotest +12 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/.travis.yml +13 -0
- data/.yardopts +3 -0
- data/Gemfile +23 -0
- data/Guardfile +6 -0
- data/LICENSE +22 -0
- data/Manifest +25 -0
- data/Rakefile +15 -0
- data/autotest/discover.rb +1 -0
- data/changelog.md +4 -0
- data/edh.gemspec +28 -0
- data/lib/edh.rb +47 -0
- data/lib/edh/api.rb +93 -0
- data/lib/edh/api/rest_api.rb +58 -0
- data/lib/edh/errors.rb +78 -0
- data/lib/edh/http_service.rb +142 -0
- data/lib/edh/http_service/multipart_request.rb +40 -0
- data/lib/edh/http_service/response.rb +18 -0
- data/lib/edh/test_users.rb +188 -0
- data/lib/edh/utils.rb +20 -0
- data/lib/edh/version.rb +3 -0
- data/readme.md +42 -0
- data/spec/cases/api_spec.rb +143 -0
- data/spec/cases/edh_spec.rb +64 -0
- data/spec/cases/edh_test_spec.rb +5 -0
- data/spec/cases/error_spec.rb +104 -0
- data/spec/cases/http_service_spec.rb +324 -0
- data/spec/cases/multipart_request_spec.rb +66 -0
- data/spec/cases/utils_spec.rb +24 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/support/custom_matchers.rb +28 -0
- data/spec/support/edh_test.rb +185 -0
- data/spec/support/mock_http_service.rb +124 -0
- data/spec/support/ordered_hash.rb +201 -0
- data/spec/support/rest_api_shared_examples.rb +114 -0
- metadata +182 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'edh/http_service/multipart_request'
|
3
|
+
require 'edh/http_service/response'
|
4
|
+
|
5
|
+
module EDH
|
6
|
+
module HTTPService
|
7
|
+
class << self
|
8
|
+
# A customized stack of Faraday middleware that will be used to make each request.
|
9
|
+
attr_accessor :faraday_middleware
|
10
|
+
attr_accessor :http_options
|
11
|
+
end
|
12
|
+
|
13
|
+
@http_options ||= {}
|
14
|
+
|
15
|
+
# EDH's default middleware stack.
|
16
|
+
# We encode requests in a Passport-compatible multipart request,
|
17
|
+
# and use whichever adapter has been configured for this application.
|
18
|
+
DEFAULT_MIDDLEWARE = Proc.new do |builder|
|
19
|
+
builder.use EDH::HTTPService::MultipartRequest
|
20
|
+
builder.request :url_encoded
|
21
|
+
builder.adapter Faraday.default_adapter
|
22
|
+
end
|
23
|
+
|
24
|
+
# The address of the appropriate Passport server.
|
25
|
+
#
|
26
|
+
# @param options various flags to indicate which server to use.
|
27
|
+
# @option options :rest_api use the old REST API instead of the Graph API
|
28
|
+
# @option options :use_ssl force https, even if not needed
|
29
|
+
#
|
30
|
+
# @return a complete server address with protocol
|
31
|
+
def self.server(options = {})
|
32
|
+
if options[:server]
|
33
|
+
options[:server]
|
34
|
+
else
|
35
|
+
server = Passport::REST_SERVER
|
36
|
+
"#{options[:use_ssl] ? "https" : "http"}://#{server}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Makes a request directly to Passport.
|
41
|
+
# @note You'll rarely need to call this method directly.
|
42
|
+
#
|
43
|
+
# @see EDH::Passport::API#api
|
44
|
+
# @see EDH::Passport::RestAPIMethods#rest_call
|
45
|
+
#
|
46
|
+
# @param path the server path for this request
|
47
|
+
# @param args (see EDH::Passport::API#api)
|
48
|
+
# @param verb the HTTP method to use.
|
49
|
+
# If not get or post, this will be turned into a POST request with the appropriate :method
|
50
|
+
# specified in the arguments.
|
51
|
+
# @param options (see EDH::Passport::API#api)
|
52
|
+
#
|
53
|
+
# @raise an appropriate connection error if unable to make the request to Passport
|
54
|
+
#
|
55
|
+
# @return [EDH::HTTPService::Response] a response object representing the results from Passport
|
56
|
+
def self.make_request(path, args, verb, options = {})
|
57
|
+
# if the verb isn't get or post, send it as a post argument
|
58
|
+
args.merge!({:method => verb}) && verb = "post" if verb != "get" && verb != "post"
|
59
|
+
|
60
|
+
# turn all the keys to strings (Faraday has issues with symbols under 1.8.7)
|
61
|
+
params = args.inject({}) {|hash, kv| hash[kv.first.to_s] = kv.last; hash}
|
62
|
+
|
63
|
+
# figure out our options for this request
|
64
|
+
request_options = {:params => (verb == "get" ? params : {})}.merge(http_options || {}).merge(process_options(options))
|
65
|
+
if request_options[:use_ssl]
|
66
|
+
ssl = (request_options[:ssl] ||= {})
|
67
|
+
ssl[:verify] = true unless ssl.has_key?(:verify)
|
68
|
+
end
|
69
|
+
|
70
|
+
# set up our Faraday connection
|
71
|
+
# we have to manually assign params to the URL or the
|
72
|
+
conn = Faraday.new(server(request_options), request_options, &(faraday_middleware || DEFAULT_MIDDLEWARE))
|
73
|
+
|
74
|
+
response = conn.send(verb, path, (verb == "post" ? params : {}))
|
75
|
+
|
76
|
+
# Log URL information
|
77
|
+
EDH::Utils.debug "#{verb.upcase}: #{path} params: #{params.inspect}"
|
78
|
+
EDH::HTTPService::Response.new(response.status.to_i, response.body, response.headers)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Encodes a given hash into a query string.
|
82
|
+
# This is used mainly by the Batch API nowadays, since Faraday handles this for regular cases.
|
83
|
+
#
|
84
|
+
# @param params_hash a hash of values to CGI-encode and appropriately join
|
85
|
+
#
|
86
|
+
# @example
|
87
|
+
# EDH.http_service.encode_params({:a => 2, :b => "My String"})
|
88
|
+
# => "a=2&b=My+String"
|
89
|
+
#
|
90
|
+
# @return the appropriately-encoded string
|
91
|
+
def self.encode_params(param_hash)
|
92
|
+
((param_hash || {}).sort_by{|k, v| k.to_s}.collect do |key_and_value|
|
93
|
+
key_and_value[1] = MultiJson.dump(key_and_value[1]) unless key_and_value[1].is_a? String
|
94
|
+
"#{key_and_value[0].to_s}=#{CGI.escape key_and_value[1]}"
|
95
|
+
end).join("&")
|
96
|
+
end
|
97
|
+
|
98
|
+
private
|
99
|
+
|
100
|
+
def self.process_options(options)
|
101
|
+
if typhoeus_options = options.delete(:typhoeus_options)
|
102
|
+
EDH::Utils.deprecate("typhoeus_options should now be included directly in the http_options hash. Support for this key will be removed in a future version.")
|
103
|
+
options = options.merge(typhoeus_options)
|
104
|
+
end
|
105
|
+
|
106
|
+
if ca_file = options.delete(:ca_file)
|
107
|
+
EDH::Utils.deprecate("http_options[:ca_file] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:ca_file]. Support for this key will be removed in a future version.")
|
108
|
+
(options[:ssl] ||= {})[:ca_file] = ca_file
|
109
|
+
end
|
110
|
+
|
111
|
+
if ca_path = options.delete(:ca_path)
|
112
|
+
EDH::Utils.deprecate("http_options[:ca_path] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:ca_path]. Support for this key will be removed in a future version.")
|
113
|
+
(options[:ssl] ||= {})[:ca_path] = ca_path
|
114
|
+
end
|
115
|
+
|
116
|
+
if verify_mode = options.delete(:verify_mode)
|
117
|
+
EDH::Utils.deprecate("http_options[:verify_mode] should now be passed inside (http_options[:ssl] = {}) -- that is, http_options[:ssl][:verify_mode]. Support for this key will be removed in a future version.")
|
118
|
+
(options[:ssl] ||= {})[:verify_mode] = verify_mode
|
119
|
+
end
|
120
|
+
|
121
|
+
options
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @private
|
126
|
+
module TyphoeusService
|
127
|
+
def self.deprecated_interface
|
128
|
+
# support old-style interface with a warning
|
129
|
+
EDH::Utils.deprecate("the TyphoeusService module is deprecated; to use Typhoeus, set Faraday.default_adapter = :typhoeus. Enabling Typhoeus for all Faraday connections.")
|
130
|
+
Faraday.default_adapter = :typhoeus
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @private
|
135
|
+
module NetHTTPService
|
136
|
+
def self.deprecated_interface
|
137
|
+
# support old-style interface with a warning
|
138
|
+
EDH::Utils.deprecate("the NetHTTPService module is deprecated; to use Net::HTTP, set Faraday.default_adapter = :net_http. Enabling Net::HTTP for all Faraday connections.")
|
139
|
+
Faraday.default_adapter = :net_http
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
|
3
|
+
module EDH
|
4
|
+
module HTTPService
|
5
|
+
class MultipartRequest < Faraday::Request::Multipart
|
6
|
+
# Passport expects nested parameters to be passed in a certain way
|
7
|
+
# Faraday needs two changes to make that work:
|
8
|
+
# 1) [] need to be escaped (e.g. params[foo]=bar ==> params%5Bfoo%5D=bar)
|
9
|
+
# 2) such messages need to be multipart-encoded
|
10
|
+
|
11
|
+
self.mime_type = 'multipart/form-data'.freeze
|
12
|
+
|
13
|
+
def process_request?(env)
|
14
|
+
# if the request values contain any hashes or arrays, multipart it
|
15
|
+
super || !!(env[:body].respond_to?(:values) && env[:body].values.find {|v| v.is_a?(Hash) || v.is_a?(Array)})
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def process_params(params, prefix = nil, pieces = nil, &block)
|
20
|
+
params.inject(pieces || []) do |all, (key, value)|
|
21
|
+
key = "#{prefix}%5B#{key}%5D" if prefix
|
22
|
+
|
23
|
+
case value
|
24
|
+
when Array
|
25
|
+
values = value.inject([]) { |a,v| a << [nil, v] }
|
26
|
+
process_params(values, key, all, &block)
|
27
|
+
when Hash
|
28
|
+
process_params(value, key, all, &block)
|
29
|
+
else
|
30
|
+
all << block.call(key, value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
# legacy support for when MultipartRequest lived directly under EDH
|
39
|
+
MultipartRequest = HTTPService::MultipartRequest
|
40
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module EDH
|
2
|
+
module HTTPService
|
3
|
+
class Response
|
4
|
+
attr_reader :status, :body, :headers
|
5
|
+
|
6
|
+
# Creates a new Response object, which standardizes the response received by Passport for use within EDH.
|
7
|
+
def initialize(status, body, headers)
|
8
|
+
@status = status
|
9
|
+
@body = body
|
10
|
+
@headers = headers
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @private
|
16
|
+
# legacy support for when Response lived directly under EDH
|
17
|
+
Response = HTTPService::Response
|
18
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'edh'
|
2
|
+
|
3
|
+
module EDH
|
4
|
+
module Passport
|
5
|
+
|
6
|
+
# Create and manage test users for your application.
|
7
|
+
# A test user is a user account associated with an app created for the purpose
|
8
|
+
# of testing the functionality of that app.
|
9
|
+
# You can use test users for manual or automated testing --
|
10
|
+
# EDH's live test suite uses test users to verify the library works with Passport.
|
11
|
+
#
|
12
|
+
# @note the test user API is fairly slow compared to other interfaces
|
13
|
+
# (which makes sense -- it's creating whole new user accounts!).
|
14
|
+
#
|
15
|
+
class TestUsers
|
16
|
+
|
17
|
+
# The application API interface used to communicate with Passport.
|
18
|
+
# @return [EDH::Passport::API]
|
19
|
+
attr_reader :api
|
20
|
+
attr_reader :app_id, :app_access_token, :secret
|
21
|
+
|
22
|
+
# Create a new TestUsers instance.
|
23
|
+
# If you don't have your app's access token, provide the app's secret and
|
24
|
+
# EDH will make a request to Passport for the appropriate token.
|
25
|
+
#
|
26
|
+
# @param options initialization options.
|
27
|
+
# @option options :app_id the application's ID.
|
28
|
+
# @option options :app_access_token an application access token, if known.
|
29
|
+
# @option options :secret the application's secret.
|
30
|
+
#
|
31
|
+
# @raise ArgumentError if the application ID and one of the app access token or the secret are not provided.
|
32
|
+
def initialize(options = {})
|
33
|
+
@app_id = options[:app_id]
|
34
|
+
@app_access_token = options[:app_access_token]
|
35
|
+
@secret = options[:secret]
|
36
|
+
unless @app_id && (@app_access_token || @secret) # make sure we have what we need
|
37
|
+
raise ArgumentError, "Initialize must receive a hash with :app_id and either :app_access_token or :secret! (received #{options.inspect})"
|
38
|
+
end
|
39
|
+
|
40
|
+
# fetch the access token if we're provided a secret
|
41
|
+
if @secret && !@app_access_token
|
42
|
+
oauth = EDH::Passport::OAuth.new(@app_id, @secret)
|
43
|
+
@app_access_token = oauth.get_app_access_token
|
44
|
+
end
|
45
|
+
|
46
|
+
@api = API.new(@app_access_token)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Create a new test user.
|
50
|
+
#
|
51
|
+
# @param installed whether the user has installed your app
|
52
|
+
# @param permissions a comma-separated string or array of permissions the user has granted (if installed)
|
53
|
+
# @param args any additional arguments for the create call (name, etc.)
|
54
|
+
# @param options (see EDH::Passport::API#api)
|
55
|
+
#
|
56
|
+
# @return a hash of information for the new user (id, access token, login URL, etc.)
|
57
|
+
def create(installed, permissions = nil, args = {}, options = {})
|
58
|
+
# Creates and returns a test user
|
59
|
+
args['installed'] = installed
|
60
|
+
args['permissions'] = (permissions.is_a?(Array) ? permissions.join(",") : permissions) if installed
|
61
|
+
@api.graph_call(test_user_accounts_path, args, "post", options)
|
62
|
+
end
|
63
|
+
|
64
|
+
# List all test users for the app.
|
65
|
+
#
|
66
|
+
# @param options (see EDH::Passport::API#api)
|
67
|
+
#
|
68
|
+
# @return an array of hashes of user information (id, access token, etc.)
|
69
|
+
def list(options = {})
|
70
|
+
@api.graph_call(test_user_accounts_path, {}, "get", options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Delete a test user.
|
74
|
+
#
|
75
|
+
# @param test_user the user to delete; can be either a Passport ID or the hash returned by {#create}
|
76
|
+
# @param options (see EDH::Passport::API#api)
|
77
|
+
#
|
78
|
+
# @return true if successful, false (or an {EDH::Passport::APIError APIError}) if not
|
79
|
+
def delete(test_user, options = {})
|
80
|
+
test_user = test_user["id"] if test_user.is_a?(Hash)
|
81
|
+
@api.delete_object(test_user, options)
|
82
|
+
end
|
83
|
+
|
84
|
+
# Deletes all test users in batches of 50.
|
85
|
+
#
|
86
|
+
# @note if you have a lot of test users (> 20), this operation can take a long time.
|
87
|
+
#
|
88
|
+
# @param options (see EDH::Passport::API#api)
|
89
|
+
#
|
90
|
+
# @return a list of the test users that have been deleted
|
91
|
+
def delete_all(options = {})
|
92
|
+
# ideally we'd save a call by checking next_page_params, but at the time of writing
|
93
|
+
# Passport isn't consistently returning full pages after the first one
|
94
|
+
previous_list = nil
|
95
|
+
while (test_user_list = list(options)).length > 0
|
96
|
+
break if test_user_list == previous_list
|
97
|
+
|
98
|
+
test_user_list.each_slice(50) do |users|
|
99
|
+
self.api.batch(options) {|batch_api| users.each {|u| batch_api.delete_object(u["id"]) }}
|
100
|
+
end
|
101
|
+
|
102
|
+
previous_list = test_user_list
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Updates a test user's attributes.
|
107
|
+
#
|
108
|
+
# @note currently, only name and password can be changed;
|
109
|
+
# see {http://developers.facebook.com/docs/test_users/ the Facebook documentation}.
|
110
|
+
#
|
111
|
+
# @param test_user the user to update; can be either a Facebook ID or the hash returned by {#create}
|
112
|
+
# @param args the updates to make
|
113
|
+
# @param options (see EDH::Passport::API#api)
|
114
|
+
#
|
115
|
+
# @return true if successful, false (or an {EDH::Passport::APIError APIError}) if not
|
116
|
+
def update(test_user, args = {}, options = {})
|
117
|
+
test_user = test_user["id"] if test_user.is_a?(Hash)
|
118
|
+
@api.graph_call(test_user, args, "post", options)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Make two test users friends.
|
122
|
+
#
|
123
|
+
# @note there's no way to unfriend test users; you can always just create a new one.
|
124
|
+
#
|
125
|
+
# @param user1_hash one of the users to friend; the hash must contain both ID and access token (as returned by {#create})
|
126
|
+
# @param user2_hash the other user to friend
|
127
|
+
# @param options (see EDH::Passport::API#api)
|
128
|
+
#
|
129
|
+
# @return true if successful, false (or an {EDH::Passport::APIError APIError}) if not
|
130
|
+
def befriend(user1_hash, user2_hash, options = {})
|
131
|
+
user1_id = user1_hash["id"] || user1_hash[:id]
|
132
|
+
user2_id = user2_hash["id"] || user2_hash[:id]
|
133
|
+
user1_token = user1_hash["access_token"] || user1_hash[:access_token]
|
134
|
+
user2_token = user2_hash["access_token"] || user2_hash[:access_token]
|
135
|
+
unless user1_id && user2_id && user1_token && user2_token
|
136
|
+
# we explicitly raise an error here to minimize the risk of confusing output
|
137
|
+
# if you pass in a string (as was previously supported) no local exception would be raised
|
138
|
+
# but the Passport call would fail
|
139
|
+
raise ArgumentError, "TestUsers#befriend requires hash arguments for both users with id and access_token"
|
140
|
+
end
|
141
|
+
|
142
|
+
u1_graph_api = API.new(user1_token)
|
143
|
+
u2_graph_api = API.new(user2_token)
|
144
|
+
|
145
|
+
u1_graph_api.graph_call("#{user1_id}/friends/#{user2_id}", {}, "post", options) &&
|
146
|
+
u2_graph_api.graph_call("#{user2_id}/friends/#{user1_id}", {}, "post", options)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Create a network of test users, all of whom are friends and have the same permissions.
|
150
|
+
#
|
151
|
+
# @note this call slows down dramatically the more users you create
|
152
|
+
# (test user calls are slow, and more users => more 1-on-1 connections to be made).
|
153
|
+
# Use carefully.
|
154
|
+
#
|
155
|
+
# @param network_size how many users to create
|
156
|
+
# @param installed whether the users have installed your app (see {#create})
|
157
|
+
# @param permissions what permissions the users have granted (see {#create})
|
158
|
+
# @param options (see EDH::Passport::API#api)
|
159
|
+
#
|
160
|
+
# @return the list of users created
|
161
|
+
def create_network(network_size, installed = true, permissions = '', options = {})
|
162
|
+
users = (0...network_size).collect { create(installed, permissions, options) }
|
163
|
+
friends = users.clone
|
164
|
+
users.each do |user|
|
165
|
+
# Remove this user from list of friends
|
166
|
+
friends.delete_at(0)
|
167
|
+
# befriend all the others
|
168
|
+
friends.each do |friend|
|
169
|
+
befriend(user, friend, options)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
return users
|
173
|
+
end
|
174
|
+
|
175
|
+
# The Passport test users management URL for your application.
|
176
|
+
def test_user_accounts_path
|
177
|
+
@test_user_accounts_path ||= "/#{@app_id}/accounts/test-users"
|
178
|
+
end
|
179
|
+
|
180
|
+
# @private
|
181
|
+
# Legacy accessor for before GraphAPI was unified into API
|
182
|
+
def graph_api
|
183
|
+
EDH::Utils.deprecate("the TestUsers.graph_api accessor is deprecated and will be removed in a future version; please use .api instead.")
|
184
|
+
@api
|
185
|
+
end
|
186
|
+
end # TestUserMethods
|
187
|
+
end # Passport
|
188
|
+
end # EDH
|
data/lib/edh/utils.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
module EDH
|
2
|
+
module Utils
|
3
|
+
|
4
|
+
# Utility methods used by EDH.
|
5
|
+
require 'logger'
|
6
|
+
require 'forwardable'
|
7
|
+
|
8
|
+
extend Forwardable
|
9
|
+
extend self
|
10
|
+
|
11
|
+
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :level, :level=
|
12
|
+
|
13
|
+
# The EDH logger, an instance of the standard Ruby logger, pointing to STDOUT by default.
|
14
|
+
# In Rails projects, you can set this to Rails.logger.
|
15
|
+
attr_accessor :logger
|
16
|
+
self.logger = Logger.new(STDOUT)
|
17
|
+
self.logger.level = Logger::ERROR
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/lib/edh/version.rb
ADDED
data/readme.md
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
####config/initializers
|
2
|
+
#### defaults to using production
|
3
|
+
```ruby
|
4
|
+
$passport_client = EDH::Passport::API.new
|
5
|
+
```
|
6
|
+
|
7
|
+
####optional access_token
|
8
|
+
```ruby
|
9
|
+
$passport_client = EDH::Passport::API.new(nil, "http://dummy-passport.dev")
|
10
|
+
```
|
11
|
+
####set the user token
|
12
|
+
```ruby
|
13
|
+
$passport_client.access_token = "abc"
|
14
|
+
```
|
15
|
+
|
16
|
+
####create returns an action id
|
17
|
+
```ruby
|
18
|
+
$passport_client.create("pages.fundraise", {:abc => "def", :cats => "dogs"})
|
19
|
+
=> 1234
|
20
|
+
#sending json example
|
21
|
+
$passport_client.create("pages.fundraise", "{\"abc\":\"def\",\"cats\":\"dogs\"}")
|
22
|
+
```
|
23
|
+
|
24
|
+
####update an action
|
25
|
+
```ruby
|
26
|
+
$passport_client.update(1234, {:abc => "12345", :cats => "6789"})
|
27
|
+
```
|
28
|
+
|
29
|
+
####delete an action
|
30
|
+
```ruby
|
31
|
+
$passport_client.delete(1234)
|
32
|
+
```
|
33
|
+
|
34
|
+
|
35
|
+
Testing
|
36
|
+
-----
|
37
|
+
|
38
|
+
Unit tests are provided for all of EDH's methods. By default, these tests run against mock responses and hence are ready out of the box:
|
39
|
+
```bash
|
40
|
+
# From anywhere in the project directory:
|
41
|
+
bundle exec rake spec
|
42
|
+
```
|