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,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
|