Rails - confused by URL change when model validations fail - ruby-on-rails

I've got a User resource where :name is a required attribute on the model.
If I try to create a new user without a name, then the validation fails and the error messages are displayed at the top of the form as expected, but the URL of the page changes from /users/new, to /users?
I hadn't noticed this behaviour until tonight when I started playing around with capybara for the first time, and was expecting the current_path after a validation failure to be http://localhost:3000/users/new
I couldn't figure out why my spec was failing:
it 'should not create an invalid user' do
fill_in "Name", :with=>""
click_button "Create User"
current_path.should == new_users_path
end
I've verified that it happens in all my other rails apps, so I realise this is the way rails works, but I really don't get what's going on here. Why does it work like this? Why does the path change from new_users_path to users_path when validations fail?
This has confused me immensely

It's perfectly normal.
In a basic CRUD, you're creating your users using a POST request to /users.
If validation fails, you just render the edit view, but it doesn't change the url.
To change the url, you should redirect_to but, this way you'd loose the info related to the performed validation.

Related

Failing Rails test based on lack of set session variable - how to set in Rails 5.2?

I have the following very simple test which has always passed.
test "should get index" do
sign_in #user
get companies_url
assert_response :success
end
However, I'm now receiving the following error.
Error:
CompaniesControllerTest#test_should_get_index:
ActionView::Template::Error: No route matches {:action=>"show", :controller=>"companies", :id=>nil}, missing required keys: [:id]
app/views/layouts/application.html.erb:53:in `_app_views_layouts_application_html_erb__2582383126246718868_70101260952060'
test/controllers/companies_controller_test.rb:16:in `block in <class:CompaniesControllerTest>'
What I've changed in my app is that I've built a sidebar (which I load via application.html.erb so it's loaded on all views (new, show, edit), which lets a user switch between various companies they "own" - which changes a session variable which we use to alter the content of the sidebar.
If we dig into the line that seems to be failing app/views/layouts/application.html.erb:53 this is how it looks:
<div class="list-group-item">
<%= link_to 'Company', company_path(session[:current_company]) unless current_user.companies.empty? %>
</div>
If I remove this link_to line then the tests pass.
My guess is that the show view is trying to load, including the sidebar which doesn't have a session[:current_company] set so the view crashes. However, in Rails 5.2 you cannot set/test session variables as far as I understand, so I'm wondering what the best way for me to set the testing up to make this pass? I do set a value for this session within my application controller a user signs in though:
def after_sign_in_path_for(resource_or_scope)
# Set a default current company scope for a use after signing in
session[:current_company] = current_user.companies.first.id unless current_user.companies.empty?
companies_path
end
Perhaps in the link_to from from within the sidebar I could add a default value to make sure we're always sending in a company, regardless of whether it's the session[:current_company] or not?
Altering the line to skip creating the link if there is no session variable present
<%= link_to 'Company', company_path(session[:current_company]) unless current_user.companies.empty? || !session[:current_company] %>
Seemed to do the trick and make all the tests pass. Would love some feedback on whether this is a good solution or not though! :)

Gracefully handling InvalidAuthenticityToken exceptions in Rails 4

I've just upgraded an app from Rails 3 to Rails 4, and I'm seeing a bunch of InvalidAuthenticityToken exceptions popping up. Digging in it looks like it is fairly common for our users to have multiple long-lived tabs open on our site. So I think what's happening is something like this: user Alice has three tabs open and her session expires. She logs back in on one of the tabs, which updates the authenticity token stored in her session. Then she returns to one of the other open tabs and tries to submit data, but she gets a 500 error from the InvalidAuthenticityToken error we raised.
It would clearly be nice to do some error handling for Alice so she doesn't get a 500 error. I'm wondering about best practices for this kind of situation. What would be a nice way to handle Alice's submission from the expired tab? I don't want to expire the current session, because that would be super annoying from the user's perspective ("I just logged in, you dolt!"). Ideally, I just want the user to reload the page, which would result in the correct authenticity token being present in the form. Or should I be doing something different so that the long-lived tabs that are open notice that the session has expired and force a reload? This would probably be sub-optimal from the user's point of view, because they liked having that page ready and easily accessible to keep referencing, which is why they left it open in the tab in the first place.
In my rails 4.1.1 app I had the same problem. I solved it by adding this code to ApplicationController. I found this solution here.
rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_referer_or_path
def redirect_to_referer_or_path
flash[:notice] = "Please try again."
redirect_to request.referer
end
This way any controller that inherits from ApplicationController will handle the error with a redirect to the page the form was submitted from with a flash message to give the user some indication of what went wrong. Note this uses the hash syntax introduced in Ruby 1.9. For older versions of Ruby you will need to use :with => :redirect_to_referer_or_path
The solution to this problem can be divided into 2 phases. Phase 1 addresses the issue of ActionController::InvalidAuthenticityToken error and phase 2 deals with the issue of long tabs waiting idly.
Phase 1(1st variation)
One way to go about is redirect the user back to their location before the error. For ex. if Alice has 3 tabs open, the first one expires and Alice logs in again in it because she was browsing on it. But when she moves to tab 3 which has URL 'http://example.com/ex' and submits a form. Now instead of displaying her an error we can redirect her back to 'http://example.com/ex' with her submitted form values already pre-filled in the form for easy use.
This can be achieved by following this approach:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.' # show this error in your layout
referrer_url = URI.parse(request.referrer) rescue URI.parse(some_default_url)
# need to have a default in case referrer is not given
# append the query string to the referrer url
referrer_url.query = Rack::Utils.parse_nested_query('').
merge(params[params.keys[2]]). # this may be different for you
to_query
# redirect to the referrer url with the modified query string
redirect_to referrer_url.to_s
end
2) You need to include a default value for all your form fields. It will be the name of that field.
...
<% f.text_field, name: 'email', placeholder: 'Email', value: params[:email] %>
...
This way whenever Alice will submit a form with wrong authenticity_token she will be redirected back to her form with the original values she submitted and she will be shown a flash message that kindly retry your request.
Phase 1(2nd variation)
Another way to go about is just redirect Alice back to the form which she submitted without any pre-filled values.
This approach can be achieved by:
1) ApplicationController - Add this function:
def handle_unverified_request
flash[:error] = 'Kindly retry.'
redirect_to :back
end
Phase 2
To tackle the problem of long awaited tabs you can take the help of SSEs. Rails 4 has ActionController::Live for handling SSEs.
1) Add this to any controller:
include ActionController::Live
...
def sse
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream, retry: 2000, event: 'refresh') # change the time interval to your suiting
if user_signed_in? # check if user is signed-in or not
sse.write('none')
else
sse.write('refresh')
end
ensure
sse.close
end
2) Give the above function a GET route in your routes file. Lets call this route '/sse'
3) Add this in your layout:
<% if user_signed_in? %> # check if user is signed-in or not
<script>
var evtSource = new EventSource("/sse");
evtSource.addEventListener('refresh', function(e){
if(e.data == 'refresh'){
window.location = window.location.href;
}
});
</script>
<% end %>
Note: using EventSource is not supported by all browsers. Please check out the Browser compatibility section.
Source:
rails 4 redirect back with new params & MDN: Using server-sent events
Answer by Durrell is perfectly alright, I am just providing an alternative way to write the same thing. This needs to go in ApplicationController.
rescue_from ActionController::InvalidAuthenticityToken do |_exception|
flash[:alert] = 'Please try again.'
redirect_back fallback_location: root_path
end

Ruby on rails: Cucumber w/Capybara goes to ApplicationController on redirect_to

I'm trying to set up a RESTful API to a database of redirect links. I have set up a lot of tests in cucumber one of which is when a user does a GET on /links/:id. This is supposed to redirect the user to the link. It works in the browser but I'm having some trouble setting up this test in cucumber.
Given /^The link id part of the URL matches an existing entry in the links table$/ do
FactoryGirl.create(:users)
FactoryGirl.create(:links, :OWNER_USERID => Users.first.id)
Users.count.should==1
Links.count.should==1
end
When /^you use GET on link$/ do
visit link_path(Links.first.id)
end
The link_path specified sends me to this show method:
def show
redir=Links.find_by_id(params[:id])
redirect_to redir.target, :status=>307
end
The problem is just that cucumber fails on the When part complaining that I doesn't have a template for application/index. For some reason it does not redirect me but rather goes to root_path of my own site. Anyone knows how to check if such a redirect actually works?
Have you tried testing it in RSpec instead of Cucumber? Try something like this and see if it works:
describe "testing links" do
subject { page }
describe "use GET on link" do
let(:links) { Links.first }
before { get link_path(Links.first.id) }
specify { response.should redirect_to("http://example.com") }
end
end
Also make sure your test database if properly configured and populated before doing the tests.
rake db:migrate and rake db:test:prepare

Rails - Losing session with Integration Tests and Capybara - CSRF related?

I'm using Rails 3.1.0.rc4 and I'm working on doing integration tests with capybara's new Steak-like DSL and Rspec (using Devise authentication)
The issue I'm having is that when I run an integration test, the rack-test driver from capybara seems to just completely lose the user's logged in session, in fact, the session seems to just clear out altogether.
After days of debugging, I'm at a complete loss as to why. Going line by line through the middleware stack, I believe I've ruled the problem down to something going on in the ActiveRecord::SessionStore that is causing this. I've read here that Rails will clear out a session if it can't validate the CSRF token, which leaves me to believe that I've got something configured wrong, and for some reason this one test is not authenticating the CSRF token correctly.
This is what is in my session_store.rb in the /initializers directory:
MyApp::Application.config.session_store :active_record_store
Does anyone who knows about CSRF protection in rails have any leads on why this may be happening?
Also, here are some things to note:
the thing I'm trying to test actually works within the browser itself, only this one test is dropping the session
the session seems to get dropped after the submission of a form to which the action url is to another server. I'm using the VCR gem for capturing the requests/responses to this external server in the test, and while I believe I've ruled the external request as the problem, this may have something directly to do with the CSRF token not authenticating, thus clearing out the session.
other tests involving logging in / using sessions are not dropping sessions
Can anyone give me any leads as to what is going on here exactly, and why the one test just seems to arbitrarily drop its session and fail on me? I've done lots of debugging and have tried everything I can possible think of.
I'm new to capybara too and I was having a similar problem.
I was trying to login a user doing something like this:
post user_session_path, :user => {:email => user.email, :password => 'superpassword'}
And that was working ok until I tried to do something with capybara, such as visiting a page and just testing if the user was logged in. This simple test was not passing:
visit root_path
page.should have_content("logout") #if the user is logged in then the logout link should be present
At first I thought capybara was clearing the sessions but I was wrong. The thing that took me some time to realize is that the driver capybara is using handles its own sessions, so, from the point of view of capybara my user was never logged in. To do so you have to do it like this
page.driver.post user_session_path, :user => {:email => user.email, :password => 'superpassword'}
Not sure if this is your case, but hope that helps.
I was able to fix this error by setting this value to true in config/initializers/test.rb
# Disable request forgery protection in test environment
config.action_controller.allow_forgery_protection = true
Beforehand, the CSRF <meta> tags were not printing out to the <head>. After changing this value they finally appear.
The manual way of doing it is very simple:
it "does something after login" do
password = "secretpass"
user = Factory(:user, :password => password)
visit login_path
fill_in "Email", :with => user.email
fill_in "Password", :with => password
click_button "Log in"
visit # somewhere else and do something
end
You can then break this out into a function in your 'spec_helper.rb':
# at the bottom of 'spec_helper.rb'
def make_user_and_login
password = "secretpass"
#user = Factory(:user, :password => password)
visit login_path
fill_in "Email", :with => #user.email
fill_in "Password", :with => password
click_button "Log in"
end
and use it in any of your tests (probably request specs):
it "does something after login" do
make_user_and_login
# now test something that requires a logged in user
# you have access to the #user instance variable
end
This might be a long shot but I believe we end up in a bad state after click_button 'Sign in' and calling visit elsewhere immediately after.
My theory is that when we click the button the request hasn't completed yet, and we kill it by visiting another path.
From the Capybara documentation:
When issuing instructions to the DSL such as:
click_link('foo')
click_link('bar')
expect(page).to have_content('baz')
If clicking on the foo link triggers an asynchronous process, such as an Ajax request, which, when complete will add the bar link to the page, clicking on the bar link would be expected to fail, since that link doesn't exist yet. However Capybara is smart enough to retry finding the link for a brief period of time before giving up and throwing an error.
If this is the case the solution is simple: give Capybara something to look for and let it wait until the request is complete. This can be as simple as adding:
expect(page).to have_text('Signed in as bob#example.com')

Capybara + Webkit: How to test client side validations - "required" input elements?

Using Rspec and Capybara, I'm trying to test a failing validation for a form, where a "required" input is not filled in, so it fails. New navigators understanding HTML5 provide built-in validations, and I understand Capybara is using that as well. Before, I was using
page.should have_error
which doesn't work for me anymore.
Someone knows how to test this now?
Many thanks!
David
HTML5 client side validations are tricky to find. I found this post with a great answer.
The code is:
describe "when I leave required field empty" do
it "I get an the correct html5 validation error" do
#Leave the field empty
click_on "Save" # or whichever button triggers the submit
message = page.find("#field_id_attr").native.attribute("validationMessage")
expect(message).to eq "Please fill out this field."
end
end
Basically the way it works is that the field element has an attribute called "validationMessage" and the process is:
Click submit - this triggers the error message
Get a reference to the native(html) attribute(as opposed to the Capybara page object attribute) called "validationMessage". This will give you the value or the message itself.
Assert that the value is as expected.
I am not familiar with RSpec so I am not sure about what does have_error.
You should think about what you want to test exactly.
You surely don't want to test the exact behavior (what message is displayed, and how) as it is specific to each browser. What you want to test, because this is not specific to the browser, is the fact that the form is not submitted.
For instance, for a basic html form at root, with a required radio button "My value".
# Check form can not be submitted without the radio button
visit '/'
click_button 'Submit'
assert_equal '/', current_path
# Check form can be submitted with the radio button
visit '/'
choose 'My value'
click_button 'Submit'
assert_equal '/next', current_path
You should also consider to test only the presence of required in your html code, as the browser is supposed to work as expected (test only your code, not other's code)
If there is an error message, you can something along the lines of
page.should have_content("error")
This depends on how you handle the errors, and whether you use javascript or not.
This is an old post, however I will try to answer it
have_error is a method provided by webkit, to check e.g. if ajax requests or javascript in general running fine
I use to test my validations in my model specs:
describe 'validations' do
it { is_expected.to validate_presence_of :competitor_name }
it { is_expected.to validate_presence_of :chassi }
it { is_expected.to validate_presence_of :auction }
it { is_expected.to validate_presence_of :car_template_id }
end
or like
expect(FactoryGirl.create(:customer)).to be_valid
to check if my Factory is valid.
If you need to check your notices by targeting invalid inputs, you could test the html of your notice by capybara with the following:
it 'searches for specific order_car by chassi and model' do
visit order_cars_search_detailed_path
fill_in 'order_car_chassi', with: '123456'
select 'Octavia', from: 'order_car_car_template_car_template_id'
click_button 'Search Order'
expect(page).to have_content('THIS IS MY NOTICE')
expect(page).to have_content('123456')
end
Hope I could help some others running into this question.

Resources