Closed evgeniy-trebin closed 6 years ago
I am also having trouble writing request specs for apis protected by api_auth. Any suggestions on how to do it would help.
I resolved it next way
# lib/query_params.rb
module QueryParams
def self.encode(value, key = nil)
case value
when Hash then
value.map {|k, v| encode(v, append_key(key, k)) }.join('&')
when Array then
value.map {|v| encode(v, "#{key}[]") }.join('&')
when nil then
key.to_s
else
"#{key}=#{CGI.escape(value.to_s)}"
end
end
def self.append_key(root_key, key)
root_key.nil? ? key : "#{root_key}[#{key}]"
end
end
#spec/support/request_helpers.rb
require Rails.root.join('lib/query_params')
module RequestHelpers
extend ActiveSupport::Concern
included do
def auth_headers(verb, path, params = {})
query_string = ''
full_path = path
if verb.to_s == 'get' && params.any?
query_string = QueryParams.encode(params)
full_path = [path, query_string].compact.join('?')
end
url = "http://#{host}#{full_path}"
uri = URI.parse(url)
request = "Net::HTTP::#{verb.capitalize}".constantize.new(uri.request_uri)
env = {
'REQUEST_METHOD' => verb.to_s.upcase,
'SERVER_NAME' => 'www.example.com',
'SERVER_PORT' => 80,
'QUERY_STRING' => query_string,
'PATH_INFO' => path,
'HTTPS' => 'off',
'SCRIPT_NAME' => '',
'REMOTE_ADDR' => '127.0.0.1',
'REQUEST_URI' => path,
'HTTP_HOST' => 'www.example.com',
'HTTP_COOKIE' => '',
'ORIGINAL_FULLPATH' => full_path,
'ORIGINAL_SCRIPT_NAME' => ''
}
request.body = QueryParams.encode(params) if params.any?
request.add_field('CONTENT_TYPE', 'application/x-www-form-urlencoded')
signed_request = ApiAuth.sign!(request, admin_user.id, admin_user.api_secret_key)
headers = {}
signed_request.each_header {|k, v| headers[k] = v }
env.merge(headers)
end
end
end
#rails_helper.rb
RSpec.configure do |config|
config.include RequestHelpers, type: :request
end
describe 'Cities API', type: :request do
let(:cities) { City.all }
let(:admin_user) { Factory.create(:admin_user) }
let(:path) { '/api/cities' }
before { Factory.create(:route) }
let(:request_block) do
proc {|headers| get path, {}, headers }
end
context 'given an unauthorized request' do
it 'returns 401 status' do
request_block.call
expect(response).to have_http_status(:unauthorized)
end
end
context 'given an authorized request' do
it 'sends a list of cities' do
request_block.call(auth_headers(:get, path))
expect(response).to be_success
end
end
end
Hey, there is one that I'm using, working with rails (5.0.0.rc1), rspec (3.1.0):
# rspec/support/helper_methods.rb
#
# sign request with user
#
def sign_request(user, path, type = :get)
sign_request_raw(user.id, user.auth_token, path, type)
end
#
# sign request
#
def sign_request_raw(id, auth_token, path, type = :get)
allow_any_instance_of(ActionDispatch::TestRequest).to receive(:fullpath).and_return(path)
request.env["HTTP_ACCEPT"] = "application/json"
if type != :get
request.env['CONTENT_TYPE'] = "application/x-www-form-urlencoded"
end
ApiAuth.sign!(request, id, auth_token)
end
describe "POST create" do
before(:each) do
@user = FactoryGirl.create(:user)
@friend = FactoryGirl.create(:user)
end
context "with valid data" do
before(:each) do
sign_request(@user, api_v1_friend_requests_path, :post)
post :create, params: { friend_request: { id: @friend.id } }
end
it "has current_user" do
expect(assigns(:current_user)).to eq(@user)
end
end
context "with invalid data" do
it "returns status 401 not authorized" do
post :create, params: { friend_request: { id: @friend.id } }
expect(response.status).to eq(401)
end
end
end
@Adam-Stomski I love your example, but my request tests do not have access to the request object like yours. Can you post your spec_helper or show me what to include to get access to that on a Rails 5.1 project? I had been using Rack::Test::Methods but this doesn't give us access to the request object so we can't use ApiAuth.sign!
- or is there a better way to sign a request when using Rack::Test::Methods?
I never followed up on this, but here is what I ended up doing if anyone is interested.
First, I created a helper:
module Requests
module ApiHelpers
include Rack::Test::Methods
def json
JSON.parse(last_response.body).deep_symbolize_keys
end
def result
json[:result]
end
def api_request(api_account, path, type = :get, data = nil)
sign_request_raw(api_account.id, api_account.secret_key, path, type, data)
case type
when :get
get path
when :put
put path, data
when :post
post path, data
when :delete
delete path
else
raise 'Unknown method'
end
expect(json.has_key?(:messages)).to be_truthy
expect(json[:messages].keys).to match_array [:info, :warn, :error]
if last_response.status >= 200 && last_response.status < 300
expect(json.has_key?(:result)).to be_truthy
end
last_response
end
private
def sign_request_raw(id, secret_key, path, type, data)
allow_any_instance_of(ActionDispatch::TestRequest).to receive(:fullpath).and_return(path)
timestamp = Time.now.utc.httpdate
content_type = "application/json"
content_md5 = ''
header "ACCEPT", content_type
header "CONTENT_TYPE", content_type
header "DATE", timestamp
unless data.nil?
content_md5 = Digest::MD5.hexdigest(data)
header "CONTENT_MD5", content_md5
end
# handle uri params for get requests`
if path.include?('?')
uri = path.split('?')
params = uri[1].split('&')
encoded_params = ""
params.each do |param|
next unless param.include?('=')
encoded_params += '&' if encoded_params.length.positive?
split_param = param.split('=')
encoded_params += split_param[0] + '=' + CGI.escape(split_param[1])
end
path = uri[0] + '?' + encoded_params
end
c_string = canonical_string(type, content_type, content_md5, path, timestamp)
puts "EmployeeProfile canonical_string:'#{c_string}'"
digest = OpenSSL::Digest.new('sha1')
sig = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret_key, c_string))
header "AUTHORIZATION", "APIAuth #{id}:#{sig}"
end
def canonical_string(request_method, content_type, content_md5, path, timestamp)
[request_method.upcase,
content_type,
content_md5,
path,
timestamp].join(',')
end
end
end
Next, I included that helper on all request specs. To do this, add this line to your spec_helper.rb's RSpec.configure block:
# in spec_helper.rb
RSpec.configure do |config|
# your other config is here
config.include Requests::ApiHelpers, type: :request
end
This issue can probably be closed
One distinction that took me awhile to recognize was
@Adam-Stomski's example works with type: :controller
@evgeniy-trebin's example works with type: :request
Thanks for the post and clarifications every :+1:
I have a spec type: :request and I want to add authentication via ApiAuth. How can i do that? I know that I can use it in specs for controllers as follows
But request object is missing in specs for requests
Now I stub authentication
But I want to avoid of using stubs in request specs. Does anybody have any ideas?