edh 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
```
|