I currently have tests that ensure that certain actions are not_routable with Rspec:
it 'does not route to #create' do
expect(post: '/sectors').to_not be_routable
end
it 'does not route to #show' do
expect(get: '/sectors/1').to_not be_routable
end
But I have changed the way my exceptions are handled by using rescue_from in my ApplicationController.
Routes:
get '*unmatched_route', to: 'application#raise_not_found'
Application Controller:
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActionController::RoutingError, with: :not_found
def not_found
respond_to do |format|
format.html { render :file => "#{Rails.root}/public/404", :layout => false, :status => :not_found }
format.xml { head :not_found }
format.any { head :not_found }
end
end
def raise_not_found
raise ActionController::RoutingError.new("No route matches #{params[:unmatched_route]}")
end
I can't quite work out though how to construct a test that checks the 404 page and its contents get rendered:
it 'does not route to #create' do
post: '/sectors'
expect(response.status).to eq(404)
expect(response).to render_template(:file => "#{Rails.root}/public/404.html")
end
I get an error on post: '/sectors', how do I simulate the post request in this case?
Use Request Specs for this.
Your test should look like:
it 'does not route to #create' do
post '/sectors'
expect(response.status).to eq(404)
expect(response).to render_template(:file => "#{Rails.root}/public/404.html")
end
Notice that post here is not a symbol, it is a method call.
Related
Currently i am catching the error not_found like this
def show
begin
#task = Task.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found and return
end
and the rspec test would be like this expect(response).to be_not_found
but i dont want to do that (rescue ActiveRecord::RecordNotFound => e) in every single function (update, create, destroy and so on)
there is another way?
for example this way
rescue_from ActiveRecord::RecordNotFound, with: :not_found
def not_found
respond_to do |format|
format.json { head :not_found }
end
end
but i dont know how can i test with that
i would like to test the same way
expect(response).to be_not_found
I think that your original implementation with an error node is a better response but your modified change is a better way to handle so I would suggest combining the 2 concepts via
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def not_found(exception)
respond_to do |format|
format.json do
# I expanded the response a bit but handle as you see fit
render json: {
error: {
message: 'Record Not Found',
data: {
record_type: exception.model,
id: exception.id
}
}
}, status: :not_found
end
end
end
You should be able to maintain your current test in this case while avoiding the need to individually handle in each request.
You can add the below code in your application_controller.rb.
around_filter :catch_not_found #=> only:[:show, :edit]
def catch_not_found
yield
rescue ActiveRecord::RecordNotFound => e
respond_to do |format|
format.json { render json: { error: e.to_s }, status: :not_found and return }
format.html { redirect_to root_url, :flash => { :error => "Record not found." } and return }
end
end
Below is the simple example for test cases using RSpec. Modify as per your requirements.
staff_controller.rb
def show
#staff = Staff.find(params[:id])
end
RSpec
let(:staff) { FactoryBot.create(:staff) }
describe "GET #show" do
it "Renders show page for valid staff" do
get :show, {:id => staff.to_param}
expect(response).to render_template :show
end
it "redirects to root path on staff record not_found" do
get :show, id: 100
expect(response).to redirect_to(root_path)
end
end
I'm having trouble getting my RSpec tests with CSV formatting to work.
My controller has a before_filter for authenticating the current user (devise). It works perfectly for this code get :index :format => :html however, when I need to test my get :index :format => :csv route, it is returning a 401 instead of redirecting. This is correct, the authentication is still working, but it's not redirecting.
Why isn't the controller redirecting the unauthorized person to the sign-in page when they hit http://localhost.com/rooms.csv but properly redirects the user if they hit http://localhost.com/rooms?
class RoomsController < ApplicationController
before_action :authenticate_user!
def index
respond_to do |format|
format.html do
#rooms = current_user.rooms
end
format.csv do
# all the stuff to create a CSV
end
end
end
describe 'GET index format CSV' do
let(:patrick){ Fabricate(:user) }
before { sign_in :user, patrick; patrick.confirm;}
it_behaves_like "requires sign in" do
let(:action) { get :index, :format => :csv }
end
end
Failure/Error: expect(response).to redirect_to new_user_session_path
Expected response to be a <redirect>, but was <401>
My spec is like so:
describe SomeController do
before(:each) do
#request.env["HTTP_ACCEPT"] = 'application/vnd.apple.mpegurl'
end
describe 'GET #index' do
it "returns response" do
get 'index', format: :m3u8
puts response.code # prints 406
response.should be_success # fails
end
end
end
The controller:
class SomeController < AuthenticatedController
def index
Mime::Type.register "application/vnd.apple.mpegurl", :m3u8
# do some stuff
respond_to do |format|
format.m3u8 { render :m3u8 => #some_variable.html_safe }
end
end
What am I missing to get it to respond with status 200? Right now, the status returned is 406. Thanks.
Drop the #.
before(:each) do
request.env["HTTP_ACCEPT"] = 'application/vnd.apple.mpegurl'
end
I've scoured the web, but, alas, I just can't seem to get Rspec to correctly send content-type so I can test my JSON API. I'm using the RABL gem for templates, Rails 3.0.11, and Ruby 1.9.2-p180.
My curl output, which works fine (should be a 401, I know):
mrsnuggles:tmp gaahrdner$ curl -i -H "Accept: application/json" -X POST -d #bleh http://localhost:3000/applications
HTTP/1.1 403 Forbidden
Content-Type: application/json; charset=utf-8
Cache-Control: no-cache
X-Ua-Compatible: IE=Edge
X-Runtime: 0.561638
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-02-18)
Date: Tue, 06 Mar 2012 01:10:51 GMT
Content-Length: 74
Connection: Keep-Alive
Set-Cookie: _session_id=8e8b73b5a6e5c95447aab13dafd59993; path=/; HttpOnly
{"status":"error","message":"You are not authorized to access this page."}
Sample from one of my test cases:
describe ApplicationsController do
render_views
disconnect_sunspot
let(:application) { Factory.create(:application) }
subject { application }
context "JSON" do
describe "creating a new application" do
context "when not authorized" do
before do
json = { :application => { :name => "foo", :description => "bar" } }
request.env['CONTENT_TYPE'] = 'application/json'
request.env['RAW_POST_DATA'] = json
post :create
end
it "should not allow creation of an application" do
Application.count.should == 0
end
it "should respond with a 403" do
response.status.should eq(403)
end
it "should have a status and message key in the hash" do
JSON.parse(response.body)["status"] == "error"
JSON.parse(response.body)["message"] =~ /authorized/
end
end
context "authorized" do
end
end
end
end
These tests never pass though, I always get redirected and my content-type is always text/html, regardless of how I seem to specify the type in my before block:
# nope
before do
post :create, {}, { :format => :json }
end
# nada
before do
post :create, :format => Mime::JSON
end
# nuh uh
before do
request.env['ACCEPT'] = 'application/json'
post :create, { :foo => :bar }
end
Here is the rspec output:
Failures:
1) ApplicationsController JSON creating a new application when not authorized should respond with a 403
Failure/Error: response.status.should eq(403)
expected 403
got 302
(compared using ==)
# ./spec/controllers/applications_controller_spec.rb:31:in `block (5 levels) in <top (required)>'
2) ApplicationsController JSON creating a new application when not authorized should have a status and message key in the hash
Failure/Error: JSON.parse(response.body)["status"] == "errors"
JSON::ParserError:
756: unexpected token at '<html><body>You are being redirected.</body></html>'
# ./spec/controllers/applications_controller_spec.rb:35:in `block (5 levels) in <top (required)>'
As you can see I'm getting the 302 redirect for the HTML format, even though I'm trying to specify 'application/json'.
Here is my application_controller.rb, with the rescue_from bit:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, :with => :not_found
protect_from_forgery
helper_method :current_user
helper_method :remove_dns_record
rescue_from CanCan::AccessDenied do |exception|
flash[:alert] = exception.message
respond_to do |format|
h = { :status => "error", :message => exception.message }
format.html { redirect_to root_url }
format.json { render :json => h, :status => :forbidden }
format.xml { render :xml => h, :status => :forbidden }
end
end
private
def not_found(exception)
respond_to do |format|
h = { :status => "error", :message => exception.message }
format.html { render :file => "#{RAILS_ROOT}/public/404.html", :status => :not_found }
format.json { render :json => h, :status => :not_found }
format.xml { render :xml => h, :status => :not_found }
end
end
end
And also applications_controller.rb, specifically the 'create' action which is what I'm trying to test. It's fairly ugly at the moment because I'm using state_machine and overriding the delete method.
def create
# this needs to be cleaned up and use accepts_attributes_for
#application = Application.new(params[:application])
#environments = params[:application][:environment_ids]
#application.environment_ids<<#environments unless #environments.blank?
if params[:site_bindings] == "new"
#site = Site.new(:name => params[:application][:name])
#environments.each do |e|
#site.siteenvs << Siteenv.new(:environment_id => e)
end
end
if #site
#application.sites << #site
end
if #application.save
if #site
#site.siteenvs.each do |se|
appenv = #application.appenvs.select {|e| e.environment_id == se.environment_id }
se.appenv = appenv.first
se.save
end
end
flash[:success] = "New application created."
respond_with(#application, :location => #application)
else
render 'new'
end
# super stinky :(
#application.change_servers_on_appenvs(params[:servers]) unless params[:servers].blank?
#application.save
end
I've looked at the source code here: https://github.com/rails/rails/blob/master/actionpack/lib/action_controller/metal/responder.rb, and it seems it should respond correctly, as well as a number of questions on stack overflow that seem to have similar issues and possible solutions, but none work for me.
What am I doing wrong?
I realize that setting :format => :json is one solution (as noted above). However, I wanted to test the same conditions that the clients to my API would use. My clients would not be setting the :format parameter, instead they would be setting the Accept HTTP header. If you are interested in this solution, here is what I used:
# api/v1/test_controller_spec.rb
require 'spec_helper.rb'
describe Api::V1::TestController do
render_views
context "when request sets accept => application/json" do
it "should return successful response" do
request.accept = "application/json"
get :test
response.should be_success
end
end
end
Try moving the :format key inside the params hash of the request, like this:
describe ApplicationsController do
render_views
disconnect_sunspot
let(:application) { Factory.create(:application) }
subject { application }
context "JSON" do
describe "creating a new application" do
context "when not authorized" do
it "should not allow creation of an application" do
params = { :format => 'json', :application => { :name => "foo", :description => "bar" } }
post :create, params
Expect(Application.count).to eq(0)
expect(response.status).to eq(403)
expect(JSON.parse(response.body)["status"]).to eq("error")
expect(JSON.parse(response.body)["message"]).to match(/authorized/)
end
end
context "authorized" do
end
end
end
end
Let me know how it goes! thats the way I have set my tests, and they are working just fine!
Super late answer here! While you can use render_views to get the json from the controller, you can also simply write a views test:
spec/views/posts/show.json.jbuilder_spec.rb
require 'rails_helper'
RSpec.describe 'posts/show.json', type: :view do
subject(:json) { JSON.parse render }
before { assign(:post, post) }
let(:post) { Post.create title: 'New Post' }
it 'has the title attribute' do
expect(json).to include 'title' => post.title
end
end
Is there a way to catch all uncatched exceptions in a rails controller, like this:
def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue ActiveRecord::CatchAll
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end
Thank you
You can also define a rescue_from method.
class ApplicationController < ActionController::Base
rescue_from ActionController::RoutingError, :with => :error_render_method
def error_render_method
respond_to do |type|
type.xml { render :template => "errors/error_404", :status => 404 }
type.all { render :nothing => true, :status => 404 }
end
true
end
end
Depending on what your goal is, you may also want to consider NOT handling exceptions on a per-controller basis. Instead, use something like the exception_handler gem to manage responses to exceptions consistently. As a bonus, this approach will also handle exceptions that occur at the middleware layer, like request parsing or database connection errors that your application does not see. The exception_notifier gem might also be of interest.
begin
# do something dodgy
rescue ActiveRecord::RecordNotFound
# handle not found error
rescue ActiveRecord::ActiveRecordError
# handle other ActiveRecord errors
rescue # StandardError
# handle most other errors
rescue Exception
# handle everything else
raise
end
You can catch exceptions by type:
rescue_from ::ActiveRecord::RecordNotFound, with: :record_not_found
rescue_from ::NameError, with: :error_occurred
rescue_from ::ActionController::RoutingError, with: :error_occurred
# Don't resuce from Exception as it will resuce from everything as mentioned here "http://stackoverflow.com/questions/10048173/why-is-it-bad-style-to-rescue-exception-e-in-ruby" Thanks for #Thibaut Barrère for mention that
# rescue_from ::Exception, with: :error_occurred
protected
def record_not_found(exception)
render json: {error: exception.message}.to_json, status: 404
return
end
def error_occurred(exception)
render json: {error: exception.message}.to_json, status: 500
return
end
rescue with no arguments will rescue any error.
So, you'll want:
def delete
schedule_id = params[:scheduleId]
begin
Schedules.delete(schedule_id)
rescue ActiveRecord::RecordNotFound
render :json => "record not found"
rescue
#Only comes in here if nothing else catches the error
end
render :json => "ok"
end
Error handling for a nicer user experience is a very tough thing to pull off correctly.
Here I have provided a fully-complete template to make your life easier. This is better than a gem because its fully customizable to your application.
Note: You can view the latest version of this template at any time on my website: https://westonganger.com/posts/how-to-properly-implement-error-exception-handling-for-your-rails-controllers
Controller
class ApplicationController < ActiveRecord::Base
def is_admin_path?
request.path.split("/").reject{|x| x.blank?}.first == 'admin'
end
private
def send_error_report(exception, sanitized_status_number)
val = true
# if sanitized_status_number == 404
# val = false
# end
# if exception.class == ActionController::InvalidAuthenticityToken
# val = false
# end
return val
end
def get_exception_status_number(exception)
status_number = 500
error_classes_404 = [
ActiveRecord::RecordNotFound,
ActionController::RoutingError,
]
if error_classes_404.include?(exception.class)
if current_user
status_number = 500
else
status_number = 404
end
end
return status_number.to_i
end
def perform_error_redirect(exception, error_message:)
status_number = get_exception_status_number(exception)
if send_error_report(exception, status_number)
ExceptionNotifier.notify_exception(exception, data: {status: status_number})
end
### Log Error
logger.error exception
exception.backtrace.each do |line|
logger.error line
end
if Rails.env.development?
### To allow for the our development debugging tools
raise exception
end
### Handle XHR Requests
if (request.format.html? && request.xhr?)
render template: "/errors/#{status_number}.html.erb", status: status_number
return
end
if status_number == 404
if request.format.html?
if request.get?
render template: "/errors/#{status_number}.html.erb", status: status_number
return
else
redirect_to "/#{status_number}"
end
else
head status_number
end
return
end
### Determine URL
if request.referrer.present?
url = request.referrer
else
if current_user && is_admin_path? && request.path.gsub("/","") != admin_root_path.gsub("/","")
url = admin_root_path
elsif request.path != "/"
url = "/"
else
if request.format.html?
if request.get?
render template: "/errors/500.html.erb", status: 500
else
redirect_to "/500"
end
else
head 500
end
return
end
end
flash_message = error_message
### Handle Redirect Based on Request Format
if request.format.html?
redirect_to url, alert: flash_message
elsif request.format.js?
flash[:alert] = flash_message
flash.keep(:alert)
render js: "window.location = '#{url}';"
else
head status_number
end
end
rescue_from Exception do |exception|
perform_error_redirect(exception, error_message: I18n.t('errors.system.general'))
end
end
Testing
To test this in your specs you can use the following template:
feature 'Error Handling', type: :controller do
### Create anonymous controller, the anonymous controller will inherit from stated controller
controller(ApplicationController) do
def raise_500
raise Errors::InvalidBehaviour.new("foobar")
end
def raise_possible_404
raise ActiveRecord::RecordNotFound
end
end
before(:all) do
#user = User.first
#error_500 = I18n.t('errors.system.general')
#error_404 = I18n.t('errors.system.not_found')
end
after(:all) do
Rails.application.reload_routes!
end
before :each do
### draw routes required for non-CRUD actions
routes.draw do
get '/anonymous/raise_500'
get '/anonymous/raise_possible_404'
end
end
describe "General Errors" do
context "Request Format: 'html'" do
scenario 'xhr request' do
get :raise_500, format: :html, xhr: true
expect(response).to render_template('errors/500.html.erb')
end
scenario 'with referrer' do
path = "/foobar"
request.env["HTTP_REFERER"] = path
get :raise_500
expect(response).to redirect_to(path)
post :raise_500
expect(response).to redirect_to(path)
end
scenario 'admin sub page' do
sign_in #user
request.path_info = "/admin/foobar"
get :raise_500
expect(response).to redirect_to(admin_root_path)
post :raise_500
expect(response).to redirect_to(admin_root_path)
end
scenario "admin root" do
sign_in #user
request.path_info = "/admin"
get :raise_500
expect(response).to redirect_to("/")
post :raise_500
expect(response).to redirect_to("/")
end
scenario 'public sub-page' do
get :raise_500
expect(response).to redirect_to("/")
post :raise_500
expect(response).to redirect_to("/")
end
scenario 'public root' do
request.path_info = "/"
get :raise_500
expect(response).to render_template('errors/500.html.erb')
expect(response).to have_http_status(500)
post :raise_500
expect(response).to redirect_to("/500")
end
scenario '404 error' do
get :raise_possible_404
expect(response).to render_template('errors/404.html.erb')
expect(response).to have_http_status(404)
post :raise_possible_404
expect(response).to redirect_to('/404')
sign_in #user
get :raise_possible_404
expect(response).to redirect_to('/')
post :raise_possible_404
expect(response).to redirect_to('/')
end
end
context "Request Format: 'js'" do
render_views ### Enable this to actually render views if you need to validate contents
scenario 'xhr request' do
get :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js, xhr: true
expect(response.body).to include("window.location = '/';")
end
scenario 'with referrer' do
path = "/foobar"
request.env["HTTP_REFERER"] = path
get :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '#{path}';")
end
scenario 'admin sub page' do
sign_in #user
request.path_info = "/admin/foobar"
get :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '#{admin_root_path}';")
end
scenario "admin root" do
sign_in #user
request.path_info = "/admin"
get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end
scenario 'public page' do
get :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
post :raise_500, format: :js
expect(response.body).to include("window.location = '/';")
end
scenario 'public root' do
request.path_info = "/"
get :raise_500, format: :js
expect(response).to have_http_status(500)
post :raise_500, format: :js
expect(response).to have_http_status(500)
end
scenario '404 error' do
get :raise_possible_404, format: :js
expect(response).to have_http_status(404)
post :raise_possible_404, format: :js
expect(response).to have_http_status(404)
sign_in #user
get :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")
post :raise_possible_404, format: :js
expect(response).to have_http_status(200)
expect(response.body).to include("window.location = '/';")
end
end
context "Other Request Format" do
scenario '500 error' do
get :raise_500, format: :json
expect(response).to have_http_status(500)
post :raise_500, format: :json
expect(response).to have_http_status(500)
end
scenario '404 error' do
get :raise_possible_404, format: :json
expect(response).to have_http_status(404)
post :raise_possible_404, format: :json
expect(response).to have_http_status(404)
sign_in #user
get :raise_possible_404, format: :json
expect(response).to have_http_status(500)
post :raise_possible_404, format: :json
expect(response).to have_http_status(500)
end
end
end
end
Actually, if you really want to catch everything, you just create your own exceptions app, which let's you customize the behavior that is usually handled by the PublicExceptions middleware: https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
location in stack https://github.com/rails/rails/blob/4-2-stable/railties/lib/rails/application/default_middleware_stack.rb#L98-L99
configuring https://github.com/rails/rails/blame/4-2-stable/guides/source/configuring.md#L99
which can be as easy as using the routes http://blog.plataformatec.com.br/2012/01/my-five-favorite-hidden-features-in-rails-3-2/ or a custom controller (but see https://github.com/rails/rails/pull/17815 for reasons not to use the routes)
A bunch of the other answers share gems that do this for you, but there's really no reason you can't just look at them and do it yourself.
A caveat: make sure you never raise an exception in your exception handler. Otherwise you get an ugly FAILSAFE_RESPONSE https://github.com/rails/rails/blob/4-2-stable/actionpack/lib/action_dispatch/middleware/show_exceptions.rb#L4-L22
BTW, the behavior in the controller comes from rescuable: https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/rescuable.rb#L32-L51