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,66 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EDH::HTTPService::MultipartRequest do
|
4
|
+
it "is a subclass of Faraday::Request::Multipart" do
|
5
|
+
EDH::HTTPService::MultipartRequest.superclass.should == Faraday::Request::Multipart
|
6
|
+
end
|
7
|
+
|
8
|
+
it "defines mime_type as multipart/form-data" do
|
9
|
+
EDH::HTTPService::MultipartRequest.mime_type.should == 'multipart/form-data'
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#process_request?" do
|
13
|
+
before :each do
|
14
|
+
@env = {}
|
15
|
+
@multipart = EDH::HTTPService::MultipartRequest.new
|
16
|
+
@multipart.stub(:request_type).and_return("")
|
17
|
+
end
|
18
|
+
|
19
|
+
# no way to test the call to super, unfortunately
|
20
|
+
it "returns true if env[:body] is a hash with at least one hash in its values" do
|
21
|
+
@env[:body] = {:a => {:c => 2}}
|
22
|
+
@multipart.process_request?(@env).should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns true if env[:body] is a hash with at least one array in its values" do
|
26
|
+
@env[:body] = {:a => [:c, 2]}
|
27
|
+
@multipart.process_request?(@env).should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns true if env[:body] is a hash with mixed objects in its values" do
|
31
|
+
@env[:body] = {:a => [:c, 2], :b => {:e => :f}}
|
32
|
+
@multipart.process_request?(@env).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "returns false if env[:body] is a string" do
|
36
|
+
@env[:body] = "my body"
|
37
|
+
@multipart.process_request?(@env).should be_false
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns false if env[:body] is a hash without an array or hash value" do
|
41
|
+
@env[:body] = {:a => 3}
|
42
|
+
@multipart.process_request?(@env).should be_false
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#process_params" do
|
47
|
+
before :each do
|
48
|
+
@parent = Faraday::Request::Multipart.new
|
49
|
+
@multipart = EDH::HTTPService::MultipartRequest.new
|
50
|
+
@block = lambda {|k, v| "#{k}=#{v}"}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "is identical to the parent for requests without a prefix" do
|
54
|
+
hash = {:a => 2, :c => "3"}
|
55
|
+
@multipart.process_params(hash, &@block).should == @parent.process_params(hash, &@block)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "replaces encodes [ and ] if the request has a prefix" do
|
59
|
+
hash = {:a => 2, :c => "3"}
|
60
|
+
prefix = "foo"
|
61
|
+
# process_params returns an array
|
62
|
+
@multipart.process_params(hash, prefix, &@block).join("&").should == @parent.process_params(hash, prefix, &@block).join("&").gsub(/\[/, "%5B").gsub(/\]/, "%5D")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe EDH::Utils do
|
4
|
+
describe ".logger" do
|
5
|
+
it "has an accessor for logger" do
|
6
|
+
EDH::Utils.methods.map(&:to_sym).should include(:logger)
|
7
|
+
EDH::Utils.methods.map(&:to_sym).should include(:logger=)
|
8
|
+
end
|
9
|
+
|
10
|
+
it "defaults to the standard ruby logger with level set to ERROR" do |variable|
|
11
|
+
EDH::Utils.logger.should be_kind_of(Logger)
|
12
|
+
EDH::Utils.logger.level.should == Logger::ERROR
|
13
|
+
end
|
14
|
+
|
15
|
+
logger_methods = [:debug, :info, :warn, :error, :fatal]
|
16
|
+
|
17
|
+
logger_methods.each do |logger_method|
|
18
|
+
it "should delegate #{logger_method} to the attached logger" do
|
19
|
+
EDH::Utils.logger.should_receive(logger_method)
|
20
|
+
EDH::Utils.send(logger_method, "Test #{logger_method} message")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
if RUBY_VERSION == '1.9.2' && RUBY_PATCHLEVEL < 290 && RUBY_ENGINE != "macruby"
|
2
|
+
# In Ruby 1.9.2 versions before patchlevel 290, the default Psych
|
3
|
+
# parser has an issue with YAML merge keys, which
|
4
|
+
#
|
5
|
+
# Anyone using an earlier version will see missing mock response
|
6
|
+
# errors when running the test suite similar to this:
|
7
|
+
#
|
8
|
+
# RuntimeError:
|
9
|
+
# Missing a mock response for graph_api: /me/videos: source=[FILE]: post: with_token
|
10
|
+
# API PATH: /me/videos?source=[FILE]&format=json&access_token=*
|
11
|
+
#
|
12
|
+
# For now, it seems the best fix is to just downgrade to the old syck YAML parser
|
13
|
+
# for these troubled versions.
|
14
|
+
#
|
15
|
+
# See https://github.com/tenderlove/psych/issues/8 for more details
|
16
|
+
YAML::ENGINE.yamler = 'syck'
|
17
|
+
end
|
18
|
+
|
19
|
+
# load the library
|
20
|
+
require 'edh'
|
21
|
+
|
22
|
+
# Support files
|
23
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
24
|
+
|
25
|
+
# set up our testing environment
|
26
|
+
# load testing data and (if needed) create test users or validate real users
|
27
|
+
EDHTest.setup_test_environment!
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Verifies that two URLs are equal, ignoring the order of the query string parameters
|
2
|
+
RSpec::Matchers.define :match_url do |url|
|
3
|
+
match do |original_url|
|
4
|
+
base, query_string = url.split("?")
|
5
|
+
original_base, original_query_string = original_url.split("?")
|
6
|
+
query_hash = query_to_params(query_string)
|
7
|
+
original_query_hash = query_to_params(original_query_string)
|
8
|
+
|
9
|
+
# the base URLs need to match
|
10
|
+
base.should == original_base
|
11
|
+
|
12
|
+
# the number of parameters should match (avoid one being a subset of the other)
|
13
|
+
query_hash.values.length.should == original_query_hash.values.length
|
14
|
+
|
15
|
+
# and ensure all the keys and values match
|
16
|
+
query_hash.each_pair do |key, value|
|
17
|
+
original_query_hash[key].should == value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def query_to_params(query_string)
|
22
|
+
query_string.split("&").inject({}) do |hash, segment|
|
23
|
+
k, v = segment.split("=")
|
24
|
+
hash[k] = v
|
25
|
+
hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
# small helper method for live testing
|
2
|
+
module EDHTest
|
3
|
+
|
4
|
+
class << self
|
5
|
+
attr_accessor :oauth_token, :app_id, :secret, :app_access_token, :code, :session_key
|
6
|
+
attr_accessor :search_time
|
7
|
+
attr_accessor :test_user_api
|
8
|
+
end
|
9
|
+
|
10
|
+
# Test setup
|
11
|
+
|
12
|
+
def self.setup_test_environment!
|
13
|
+
setup_rspec
|
14
|
+
|
15
|
+
unless ENV['LIVE']
|
16
|
+
# By default the EDH specs are run using stubs for HTTP requests,
|
17
|
+
# so they won't fail due to Passport-imposed rate limits or server timeouts.
|
18
|
+
#
|
19
|
+
# However as a result they are more brittle since
|
20
|
+
# we are not testing the latest responses from the Passport servers.
|
21
|
+
# To be certain all specs pass with the current Passport services,
|
22
|
+
# run LIVE=true bundle exec rake spec.
|
23
|
+
EDH.http_service = EDH::MockHTTPService
|
24
|
+
EDHTest.setup_test_data(EDH::MockHTTPService::TEST_DATA)
|
25
|
+
else
|
26
|
+
# Runs EDH specs through the Passport servers
|
27
|
+
# using data for a real app
|
28
|
+
|
29
|
+
# allow live tests with different adapters
|
30
|
+
adapter = ENV['ADAPTER'] || "typhoeus" # use Typhoeus by default if available
|
31
|
+
begin
|
32
|
+
require adapter
|
33
|
+
require 'typhoeus/adapters/faraday' if adapter.to_s == "typhoeus"
|
34
|
+
Faraday.default_adapter = adapter.to_sym
|
35
|
+
rescue LoadError
|
36
|
+
puts "Unable to load adapter #{adapter}, using Net::HTTP."
|
37
|
+
end
|
38
|
+
|
39
|
+
# use a test user unless the developer wants to test against a real profile
|
40
|
+
unless token = EDHTest.oauth_token
|
41
|
+
EDHTest.setup_test_users
|
42
|
+
else
|
43
|
+
EDHTest.validate_user_info(token)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.setup_rspec
|
49
|
+
# set up a global before block to set the token for tests
|
50
|
+
# set the token up for
|
51
|
+
RSpec.configure do |config|
|
52
|
+
config.before :each do
|
53
|
+
@token = EDHTest.oauth_token
|
54
|
+
EDH::Utils.stub(:deprecate) # never fire deprecation warnings
|
55
|
+
end
|
56
|
+
|
57
|
+
config.after :each do
|
58
|
+
# if we're working with a real user, clean up any objects posted to Passport
|
59
|
+
# no need to do so for test users, since they get deleted at the end
|
60
|
+
if @temporary_object_id && EDHTest.real_user?
|
61
|
+
raise "Unable to locate API when passed temporary object to delete!" unless @api
|
62
|
+
|
63
|
+
# wait 10ms to allow Passport to propagate data so we can delete it
|
64
|
+
sleep(0.01)
|
65
|
+
|
66
|
+
# clean up any objects we've posted
|
67
|
+
result = (@api.delete_object(@temporary_object_id) rescue false)
|
68
|
+
# if we errored out or Passport returned false, track that
|
69
|
+
puts "Unable to delete #{@temporary_object_id}: #{result} (probably a photo or video, which can't be deleted through the API)" unless result
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.setup_test_data(data)
|
76
|
+
# fix the search time so it can be used in the mock responses
|
77
|
+
self.search_time = data["search_time"] || (Time.now - 3600).to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.testing_permissions
|
81
|
+
"read_stream, publish_stream, user_photos, user_videos, read_insights"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.setup_test_users
|
85
|
+
print "Setting up test users..."
|
86
|
+
@test_user_api = EDH::Passport::TestUsers.new(:app_id => self.app_id, :secret => self.secret)
|
87
|
+
|
88
|
+
RSpec.configure do |config|
|
89
|
+
config.before :suite do
|
90
|
+
# before each test module, create two test users with specific names and befriend them
|
91
|
+
EDHTest.create_test_users
|
92
|
+
end
|
93
|
+
|
94
|
+
config.after :suite do
|
95
|
+
# after each test module, delete the test users to avoid cluttering up the application
|
96
|
+
EDHTest.destroy_test_users
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
puts "done."
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.create_test_users
|
104
|
+
begin
|
105
|
+
@live_testing_user = @test_user_api.create(true, EDHTest.testing_permissions, :name => EDHTest.user1_name)
|
106
|
+
@live_testing_friend = @test_user_api.create(true, EDHTest.testing_permissions, :name => EDHTest.user2_name)
|
107
|
+
@test_user_api.befriend(@live_testing_user, @live_testing_friend)
|
108
|
+
self.oauth_token = @live_testing_user["access_token"]
|
109
|
+
rescue Exception => e
|
110
|
+
Kernel.warn("Problem creating test users! #{e.message}")
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.destroy_test_users
|
116
|
+
[@live_testing_user, @live_testing_friend].each do |u|
|
117
|
+
puts "Unable to delete test user #{u.inspect}" if u && !(@test_user_api.delete(u) rescue false)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.validate_user_info(token)
|
122
|
+
print "Validating permissions for live testing..."
|
123
|
+
# make sure we have the necessary permissions
|
124
|
+
api = EDH::Passport::API.new(token)
|
125
|
+
perms = api.fql_query("select #{testing_permissions} from permissions where uid = me()")[0]
|
126
|
+
perms.each_pair do |perm, value|
|
127
|
+
if value == (perm == "read_insights" ? 1 : 0) # live testing depends on insights calls failing
|
128
|
+
puts "failed!\n" # put a new line after the print above
|
129
|
+
raise ArgumentError, "Your access token must have the read_stream, publish_stream, and user_photos permissions, and lack read_insights. You have: #{perms.inspect}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
puts "done!"
|
133
|
+
end
|
134
|
+
|
135
|
+
# Info about the testing environment
|
136
|
+
def self.real_user?
|
137
|
+
!(mock_interface? || @test_user_api)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.test_user?
|
141
|
+
!!@test_user_api
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.mock_interface?
|
145
|
+
EDH.http_service == EDH::MockHTTPService
|
146
|
+
end
|
147
|
+
|
148
|
+
# Data for testing
|
149
|
+
def self.user1
|
150
|
+
# user ID, either numeric or username
|
151
|
+
test_user? ? @live_testing_user["id"] : "koppel"
|
152
|
+
end
|
153
|
+
|
154
|
+
def self.user1_id
|
155
|
+
# numerical ID, used for FQL
|
156
|
+
# (otherwise the two IDs are interchangeable)
|
157
|
+
test_user? ? @live_testing_user["id"] : 2905623
|
158
|
+
end
|
159
|
+
|
160
|
+
def self.user1_name
|
161
|
+
"Alex"
|
162
|
+
end
|
163
|
+
|
164
|
+
def self.user2
|
165
|
+
# see notes for user1
|
166
|
+
test_user? ? @live_testing_friend["id"] : "lukeshepard"
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.user2_id
|
170
|
+
# see notes for user1
|
171
|
+
test_user? ? @live_testing_friend["id"] : 2901279
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.user2_name
|
175
|
+
"Luke"
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.page
|
179
|
+
"facebook"
|
180
|
+
end
|
181
|
+
|
182
|
+
def self.app_properties
|
183
|
+
mock_interface? ? {"desktop" => 0} : {"description" => "A test framework for EDH and its users. (#{rand(10000).to_i})"}
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module EDH
|
5
|
+
module MockHTTPService
|
6
|
+
include EDH::HTTPService
|
7
|
+
|
8
|
+
# fix our specs to use ok_json, so we always get the same results from to_json
|
9
|
+
MultiJson.use :ok_json
|
10
|
+
|
11
|
+
# Mocks all HTTP requests for with edh_spec_with_mocks.rb
|
12
|
+
# Mocked values to be included in TEST_DATA used in specs
|
13
|
+
ACCESS_TOKEN = '*'
|
14
|
+
APP_ACCESS_TOKEN = "**"
|
15
|
+
OAUTH_CODE = 'OAUTHCODE'
|
16
|
+
|
17
|
+
# Loads testing data
|
18
|
+
TEST_DATA = {}
|
19
|
+
TEST_DATA['search_time'] = (Time.now - 3600).to_s
|
20
|
+
|
21
|
+
RESPONSES = {}
|
22
|
+
def self.make_request(path, args, verb, options = {})
|
23
|
+
if response = match_response(path, args, verb, options)
|
24
|
+
# create response class object
|
25
|
+
response_object = if response.is_a? String
|
26
|
+
EDH::HTTPService::Response.new(200, response, {})
|
27
|
+
else
|
28
|
+
EDH::HTTPService::Response.new(response["code"] || 200, response["body"] || "", response["headers"] || {})
|
29
|
+
end
|
30
|
+
else
|
31
|
+
# Raises an error message with the place in the data YML
|
32
|
+
# to place a mock as well as a URL to request from
|
33
|
+
# Passport's servers for the actual data
|
34
|
+
# (Don't forget to replace ACCESS_TOKEN with a real access token)
|
35
|
+
data_trace = [path, args, verb, options] * ': '
|
36
|
+
|
37
|
+
args = args == 'no_args' ? '' : "#{args}&"
|
38
|
+
args += 'format=json'
|
39
|
+
|
40
|
+
raise "Missing a mock response for #{data_trace}\nAPI PATH: #{[path, args].join('?')}"
|
41
|
+
end
|
42
|
+
|
43
|
+
response_object
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.encode_params(*args)
|
47
|
+
# use HTTPService's encode_params
|
48
|
+
HTTPService.encode_params(*args)
|
49
|
+
end
|
50
|
+
|
51
|
+
protected
|
52
|
+
|
53
|
+
# For a given query, see if our mock responses YAML has a resopnse for it.
|
54
|
+
def self.match_response(path, args, verb, options = {})
|
55
|
+
server = 'rest_api'
|
56
|
+
path = 'root' if path == '' || path == '/'
|
57
|
+
verb = (verb || 'get').to_s
|
58
|
+
token = args.delete('access_token')
|
59
|
+
with_token = (token == ACCESS_TOKEN || token == APP_ACCESS_TOKEN) ? 'with_token' : 'no_token'
|
60
|
+
|
61
|
+
if endpoint = RESPONSES[server][path]
|
62
|
+
# see if we have a match for the arguments
|
63
|
+
if arg_match = endpoint.find {|query, v| decode_query(query) == massage_args(args)}
|
64
|
+
# we have a match for the server/path/arguments
|
65
|
+
# so if it's the right verb and authentication, we're good
|
66
|
+
# arg_match will be [query, hash_response]
|
67
|
+
arg_match.last[verb][with_token]
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Since we're comparing the arguments with data in a yaml file, we need to
|
73
|
+
# massage them slightly to get to the format we expect.
|
74
|
+
def self.massage_args(arguments)
|
75
|
+
args = arguments.inject({}) do |hash, (k, v)|
|
76
|
+
# ensure our args are all stringified
|
77
|
+
value = if v.is_a?(String)
|
78
|
+
should_json_decode?(v) ? MultiJson.load(v) : v
|
79
|
+
else
|
80
|
+
v
|
81
|
+
end
|
82
|
+
# make sure all keys are strings
|
83
|
+
hash.merge(k.to_s => value)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Assume format is always JSON
|
87
|
+
args.delete('format')
|
88
|
+
|
89
|
+
# if there are no args, return the special keyword no_args
|
90
|
+
args.empty? ? "no_args" : args
|
91
|
+
end
|
92
|
+
|
93
|
+
# Passport sometimes requires us to encode JSON values in an HTTP query
|
94
|
+
# param. This complicates test matches, since we get into JSON-encoding
|
95
|
+
# issues (what order keys are written into). To avoid string comparisons
|
96
|
+
# and the hacks required to make it work, we decode the query into a
|
97
|
+
# Ruby object.
|
98
|
+
def self.decode_query(string)
|
99
|
+
if string == "no_args"
|
100
|
+
string
|
101
|
+
else
|
102
|
+
# we can't use Faraday's decode_query because that CGI-unencodes, which
|
103
|
+
# will remove +'s in restriction strings
|
104
|
+
string.split("&").reduce({}) do |hash, component|
|
105
|
+
k, v = component.split("=", 2) # we only care about the first =
|
106
|
+
value = should_json_decode?(v) ? MultiJson.decode(v) : v.to_s rescue v.to_s
|
107
|
+
# some special-casing, unfortunate but acceptable in this testing
|
108
|
+
# environment
|
109
|
+
value = nil if value.empty?
|
110
|
+
value = true if value == "true"
|
111
|
+
value = false if value == "false"
|
112
|
+
hash.merge(k => value)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# We want to decode JSON because it may not be encoded in the same order
|
118
|
+
# all the time -- different Rubies may create a strings with equivalent
|
119
|
+
# content but different order. We want to compare the objects.
|
120
|
+
def self.should_json_decode?(v)
|
121
|
+
v.match(/^[\[\{]/)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|