How do i set a request.referrer inside my RSpec? - ruby-on-rails

I am trying to avoid using my session[:referred_by], and would like to use the request.referrer. However, my RSpec tests fail because the TestRequest does not store a request.referrer
So I have to do the following in order for Rspec tests to work. Is there a way to make it better:
referrer = request.referrer ? request.referrer : '/'
redirect_to referrer, :alert => error_message

ActionDispatch::TestRequest extends ActionDispatch::Request that extends Rack::Request.
The method is defined as follows
def referer
#env['HTTP_REFERER']
end
alias referrer referer
As far as I remember, you can access the environment variable in the RSpec test by using request.env. It means, it should be possible to set something like
request.env['HTTP_REFERER'] = 'http://example.com'
Of course, it depends on the type of RSpec example group you are using.

Mock it:
specify 'foo' do
controller.request.should_receive(:referer).and_return('http://example.com')
# get whatever
end
Or if you don't care if it doesn't get called, stub it:
controller.request.stub referer: 'http://example.com'

On Rails 5 the accepted solution doesn't seem to work anyore, but setting request.env['HTTP_REFERER'] directly, as Simone Carletti suggests, works.

Solution for Rails 5.2+ & RSpec 3.8+
get :endpoint, params: {}, headers: { 'HTTP_REFERER' => 'stackoverflow.com' }

With Rails4/Rspec3, in request specs, request is not available until you make an http call. But you can assign request.referer by doing something like this:
get '/posts', {}, { referer: 'http://example.com' }

This also works in controllers specs:
controller.request.headers.merge({ 'HTTP_REFERER': 'https://stackoverflow.com/' })
get(:endpoint)

For anyone else Googling this, you can also stub the request in a helper spec like this:
allow(view).to receive_message_chain(:request, :referrer).and_return("http://example.com")

Related

How to find current abstract route in Rails middware

Rails version: '~> 4.2.7.1'
Spree version: '3.1.1'
TlDr:
How do I get route as /api/products/:id or controller and action of that route in a middleware of Rails 4 application.
Details:
I am adding a middleware in my rails app which is similar to gem scout_statsd_rack. This adds following middleware to rails app to send metrics via statsd:
def call(env)
(status, headers, body), response_time = call_with_timing(env)
statsd.timing("#{env['REQUEST_PATH']}.response", response_time)
statsd.increment("#{env['REQUEST_PATH']}.response_codes.#{status.to_s.gsub(/\d{2}$/,'xx')}")
# Rack response
[status, headers, body]
rescue Exception => exception
statsd.increment("#{env['REQUEST_PATH']}.response_codes.5xx")
raise
end
def call_with_timing(env)
start = Time.now
result = #app.call(env)
[result, ((Time.now - start) * 1000).round]
end
What I want is to find current route in the middleware so that I can send metrics specific to each route.
I tried approach described here, which tells env['PATH_INFO'] can provide path, which it does, but it gives with URL params like this: /api/products/4 but what I want is /api/products/:id as my puropose is to track performance of /api/products/:id API.
env['REQUEST_PATH'] and env['REQUEST_URI'] also gives same response.
I tried answer provided here and here:
Rails.application.routes.router.recognize({"path_info" => env['PATH_INFO']})
or like this
Rails.application.routes.router.recognize(env['PATH_INFO'])
But it gave following error:
NoMethodError (undefined method path_info' for {"path_info"=>"/api/v1/products/4"}:Hash):
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:100:infind_routes'
vendor/bundle/gems/actionpack-4.2.7.1/lib/action_dispatch/journey/router.rb:59:in recognize'
vendor/bundle/gems/scout_statsd_rack-0.1.7/lib/scout_statsd_rack.rb:27:in
call'
This answer discusses request.original_url, but How do I access variable request, I think it should be same as env but not able to get route as want from this.
Edit #1
You can see the sample repo here, with code of rails middleware here, Setup of this can be done as stated in README and than this API can be hit: http://localhost:3000/api/v1/products/1.
Edit #2
I tried approach given by #MichałMłoźniak like following:
def call(env)
(status, headers, body), response_time = call_with_timing(env)
request = ActionDispatch::Request.new(env)
request = Rack::Request.new("PATH_INFO" => env['REQUEST_PATH'], "REQUEST_METHOD" => env["REQUEST_METHOD"])
Rails.application.routes.router.recognize(request) { |route, params|
puts "I am here"
puts params.inspect
puts route.inspect
}
But I got following response:
I am here
{}
#<ActionDispatch::Journey::Route:0x007fa1833ac628 #name="spree", #app=#<ActionDispatch::Routing::Mapper::Constraints:0x007fa1833ace70 #dispatcher=false, #app=Spree::Core::Engine, #constraints=[]>, #path=#<ActionDispatch::Journey::Path::Pattern:0x007fa1833acc90 #spec=#<ActionDispatch::Journey::Nodes::Slash:0x007fa1833ad230 #left="/", #memo=nil>, #requirements={}, #separators="/.?", #anchored=false, #names=[], #optional_names=[], #required_names=[], #re=/\A\//, #offsets=[0]>, #constraints={:required_defaults=>[]}, #defaults={}, #required_defaults=nil, #required_parts=[], #parts=[], #decorated_ast=nil, #precedence=1, #path_formatter=#<ActionDispatch::Journey::Format:0x007fa1833ac588 #parts=["/"], #children=[], #parameters=[]>>
I have pushed the changes as well here.
You need to pass ActionDispatch::Request or Rack::Request to recognize method. Here is an example from another app:
main:0> req = Rack::Request.new("PATH_INFO" => "/customers/10", "REQUEST_METHOD" => "GET")
main:0> Rails.application.routes.router.recognize(req) { |route, params| puts params.inspect }; nil
{:controller=>"customers", :action=>"show", :id=>"10"}
=> nil
The same will work with ActionDispatch::Request. Inside middleware, you can easily create this object:
request = ActionDispatch::Request.new(env)
And if you need more information about recognized route, you can look into that route object that is yielded to block, by recognize method.
Update
The above solution will work for normal Rails routes, but since you only have spree engine mounted you need to use different class
request = ActionDispatch::Request.new(env)
Spree::Core::Engine.routes.router.recognize(request) { |route, params|
puts params.inspect
}
I guess the best would be find a generic solution that works with any combination of normal routes and engines, but this will work in your case.
Update #2
For more general solution you need to look at the source of Rails router, which you can find in ActionDispatch module. Look at Routing and Journey modules. What I found out is that the returned route from recognize method can be tested if this is a dispatcher or not.
request = ActionDispatch::Request.new(env)
Rails.application.routes.router.recognize(req) do |route, params|
if route.dispatcher?
# if this is a dispatcher, params should have everything you need
puts params
else
# you need to go deeper
# route.app.app will be Spree::Core::Engine
route.app.app.routes.router.recognize(request) do |route, params|
puts params.inspect
}
end
end
This approach will work in case of your app, but will not be general. For example, if you have sidekiq installed, route.app.app will be Sidekiq::Web so it needs to be handled in different way. Basically to have general solution you need to handle all possible mountable engines that Rails router supports.
I guess it is better to build something that will cover all your cases in current application. So the thing to remember is that when initial request is recognized, the value of route yielded to black can be a dispatcher or not. If it is, you have normal Rails route, if not you need to recursive check.

Rails: test a helper that needs access to the Rails environment (e.g. request.fullpath)

I have a helper that accesses request.fullpath. Within an isolated helper test, request is not available. What should I do? Can I somehow mock it or something like that?
I'm using the newest versions of Rails and RSpec. Here's what my helper looks like:
def item(*args, &block)
# some code
if request.fullpath == 'some-path'
# do some stuff
end
end
So the problematic code line is #4 where the helper needs access to the request object which isn't available in the helper spec.
Thanks a lot for help.
Yes, you can mock the request. I had a whole long answer here describing how to do that, but in fact that's not necessarily what you want.
Just call your helper method on the helper object in your example. Like so:
describe "#item" do
it "does whatever" do
helper.item.should ...
end
end
That will give you access to a test request object. If you need to specify a specific value for the request path, you can do so like this:
before :each do
helper.request.path = 'some-path'
end
Actually, for completeness, let me include my original answer, since depending on what you're trying to do it might still be helpful.
Here's how you can mock the request:
request = mock('request')
controller.stub(:request).and_return request
You can add stub methods to the returned request similarly
request.stub(:method).and_return return_value
And alternative syntax to mock & stub all in one line:
request = mock('request', :method => return_value)
Rspec will complain if your mock receives messages that you didn't stub. If there's other stuff Just call your request helper method on the helper object is doing that you don't care about in your test, you can shut rspec up by making the mock a "null object",example. like Like so
request = mock('request').as_null_object
It looks like all you probably need to get your specific test passing is this:
describe "#item" do
let(:request){ mock('request', :fullpath => 'some-path') }
before :each do
controller.stub(:request).and_return request
end
it "does whatever"
end
In a helper spec, you can access the request using controller.request (so controller.request.stub(:fullpath) { "whatever" } should work)

What's the best way to set custom request headers when using Capybara in RSpec request specs?

I'm monkey patching Capybara::Session with a set_headers method that assigns to Capybara::RackTest::Browser's options attribute (which I've changed from an attr_reader to an attr_accessor).
The patches:
class Capybara::RackTest::Browser
attr_accessor :options
end
class Capybara::Session
def set_headers(headers)
if driver.browser.respond_to?(:options=) #because we've monkey patched it above
options = driver.browser.options
if options.nil? || options[:headers].nil?
options ||= {}
options[:headers] = headers
else
options[:headers].merge!(headers)
end
else
raise Capybara::NotSupportedByDriverError
end
end
end
In my request spec, I'm doing:
page.set_headers("REMOTE_ADDR" => "1.2.3.4")
visit root_path
This works, but I'm wondering if there's a better way, it seems a bit overkill to just be able to set a custom remote_ip/remote_addr on a request. Any thoughts?
If you want the headers to be globally set on all requests, you can use something like:
Capybara.register_driver :custom_headers_driver do |app|
Capybara::RackTest::Driver.new(app, :headers => {'HTTP_FOO' => 'foobar'})
end
See the rack_test_driver_spec.rb in Capybara 1.1.2 and Capybara's issue #320, Setting up HTTP headers.
Do you need to add custom header into one specific request in rspec using capybara? I used this in acceptance tests. It was the best way for me to use get method with specific header data. You can assess specific element on response page. See my example below:
get user_registration_path, { :invite => invite_token }, { 'X_GEOIP_COUNTRY_CODE' => 'US' }
expect(assigns(:ip_country)).to eq('US')
response.body.should have_selector("input#currency_usd[checked='checked']")
I hope it helps.
I've discovered an ability to modify headers when using the default Capybara::RackTest driver.
There's a method Capybara::RackTest::Browser#process which prepares a request before finaly being sent (https://www.rubydoc.info/gems/capybara/Capybara%2FRackTest%2FBrowser:process). As you can see there in the code the request headers are built from the options[:headers]. The options actually refers to the driver.options attribute. So you can set any headers by modifying this hash.
Here's an example of my feature spec with custom headers:
let(:headers) do
{
"YOUR_CUSTOM_HEADER_1" => "foo",
"YOUR_CUSTOM_HEADER_2" => "bar",
...
}
end
before(:each) do
#origin_headers = page.driver.options[:headers]
page.driver.options[:headers] ||= {}
page.driver.options[:headers].merge!(headers)
end
after(:each) do
page.driver.options[:headers] = #origin_headers
end
Tested with:
capybara: 3.13.2 (RackTest driver)
rspec: 3.8
rails: 5.2.2
P.S. Haven't tested it with selenium driver yet. But probably it works in a similar way.

Change value of request.remote_ip in Ruby on Rails

For test purposes I want to change the return value of request.remote_ip. While being on my development machine it returns always 127.0.0.1 as it should but I would like to give myself different fake IPs to test the correct behavior of my app without deploying it to an live server first!
Thank you.
If you want this functionality in your whole application, it might be better/easier to override the remote_ip method in your app/helpers/application_helper.rb:
class ActionDispatch::Request #rails 2: ActionController::Request
def remote_ip
'1.2.3.4'
end
end
And the 1.2.3.4 address is available everywhere
For integration tests, this works with rails 5:
get "/path", params: { }, headers: { "REMOTE_ADDR" => "1.2.3.4" }
You can modify the request object using:
request = ActionController::Request.new('REMOTE_ADDR' => '1.2.3.4')
request.remote_ip now returns 1.2.3.4
You can cheat a bit by making a mutator for the remote_ip value in the test environment which is normally not defined.
For instance, alter the class inside of test/test_helper.rb with the following:
class ActionController::TestRequest
def remote_ip=(value)
#env['REMOTE_ADDR'] = value.to_s
end
end
Then, during your testing you can reassign as required:
def test_something
#request.remote_ip = '1.2.3.4'
end
This can be done either in the individual test, or within your setup routine, wherever is appropriate.
I have had to use this before when writing functional tests that verify IP banning, geolocation, etc.
rails 4.0.1 rc. After hour of searching found this simple solution while digging to code :)
get '/', {}, { 'REMOTE_ADDR' => '1.2.3.4' }
What I ended up doing now was to put this code in the end of the config/environments/development.rb file to make sure it's only executed while in development
# fake IP for manuel testing
class ActionController::Request
def remote_ip
"1.2.3.4"
end
end
So this sets remote_ip to 1.2.3.4 when the server starts. Everytime you change the value you have to restart the server!
This answer is only works for rails3 (I found this answer when trying to answer a similar question for rails 3),
So I will post it here in case if someone is trying to do the same thing in Rails3 env
class ActionDispatch::Request
def remote_ip
'1.2.3.4'
end
end
HTH

foo_url(mock_foo) sometimes doesn't work in rspec tests

I am trying to write an rspec test for a controller that accesses a
model Group.
#request.env['HTTP_REFERER'] = group_url(#mock_group) ### Line 49
I get this:
NoMethodError in 'ActsController responding to create should redirect to :back'
You have a nil object when you didn't expect it!
The error occurred while evaluating nil.rewrite
/Library/Ruby/Gems/1.8/gems/actionpack-2.1.0/lib/action_controller/base.rb:621:in `url_for'
(eval):17:in `group_url'
/Library/Ruby/Gems/1.8/gems/actionpack-2.1.0/lib/action_controller/test_process.rb:464:in `send!'
/Library/Ruby/Gems/1.8/gems/actionpack-2.1.0/lib/action_controller/test_process.rb:464:in `method_missing'
This line in url_for is the problem; specfically #url is nil.
#url.rewrite(rewrite_options(options))
And it seems that #url is initialized here:
def initialize_current_url
#url = UrlRewriter.new(request, params.clone)
end
This happens because url_for depends on stuff that's initialized during request processing. I assume your test looks something like this:
it "should do whatever when referrer is group thing" do
#request.env["HTTP_REFERER"] = url_for(#mock_group)
get :some_action
"something".should == "something"
end
url_for fails because it happens before the get. The easiest way to resolve the problem is to hard-code the URL in your test (i.e. change url\_for(#mock\_group) to "http://test.host/group/1"). The other option is to figure out how to get #controller to initialize #url before you call url_for. I think I've done that before, but I don't have the code around any more and it involved digging through action_controller's code.
Look at this. I think it is relevant.
http://jakescruggs.blogspot.com/2008/11/if-you-use-mocha-and-rspec-then-read.html

Resources