Writing a rspec test for delete action in controller - ruby-on-rails

I have a delete function in one of my controllers like below:
before_action :set_form, only: [:show, :edit, :update, :destroy]
def destroy
if #form.destroy
render json: #form, status: :ok
else
render json: #form.errors, status: :not_found
end
end
private
def set_form
#form = Form.find(params[:id])
end
end
I have two questions:
1) I'm returning 404 when delete is not correctly performed. Is this reasonable status code? I looked into all 4XX status code and this was making most sense.
2) I am not sure how I am gonna write a test for render json: #form.errors, status: :not_found.
Below is what I tried:
context "Cannot delete the model" do
it "responds successfully with an HTTP 404 status code" do
delete :destroy, id: 100000
expect(response).to have_http_status(404)
end
end
The problem is that I get an error saying ActiveRecord::RecordNotFound:Couldn't find Visa with 'id'=10000 instead of delete action actually failing. How do I mock that delete action is failing?

That error is the same error you'll get in development and even production. The line...
#form = Form.find(params[:id])
Will raise an exception if no Form record is found with the delivered ID.
also if the #form is not found and you find a mechanism to continue, the #form.destroy will still not work because there's no method destroy on nil objects, and there's also no method errors on nil objects.
If you insist on handling destroy methods as you describe, you'd need to do something like...
before_action :set_form, only: [:show, :edit, :update] #remove destroy
def destroy
#form = Form.find_by(id: params[:id])
if #form
#form.destroy
render json: #form, status: :ok
else
render json: 'record not found for delete', status: :not_found
end
end

Your set_form method raises that error, you have to catch it there or use find_by_id in destroy.
def set_form
#form = Form.find(params[:id])
rescue ActiveRecord::RecordNotFound
end
This way #form will be nil in your if.
As for HTTP status code, you might consider using 422.

Rails will raise an ActiveRecord::RecordNotFound when you call .find on a record with an invalid id. This is a very desirable behaviour since it provides a nice catch-all for when a resource cannot be found.
The default is to send a 404 - not found header. For the HTML request type it also sends a default error page.
Rather the issue is how you are testing it.
If you want to test that rails raises the error:
context "Cannot delete the model" do
it "raises an error when the id is not valid" do
bypass_rescue
expect { delete :destroy, id: 100000 }.to raise_error(ActiveRecord::RecordNotFound)
expect(response).to have_http_status(404)
end
end
However if you want to test that deleting the model fails for any other reason (record is locked or user is not authorized for example) you should set up a record in the database with fixtures or factories and provide a valid id - you should also provide the correct response code (422 or 401).
As to mocking the delete failure you can use allow_any_instance_of(SomeModel).to receive(:destroy).and_return(false).

Related

URL create and new in Rails

I created a new Rails project. After all, I have a question that I cannot find anywhere or answer by myself so I need your help.
When you create a new object (like Person, Book), you need 2 action: NEW and CREATE.
When I create new, I have link: localhost:3000/admin/books/new
And then when I create fails it will return ERROR MESSAGE anf this link: localhost:3000/admin/books/create
If I click in url and `ENTER`. It will wrong.
I'm trying to use redirect_to or render if creation is failed. But nothing happen, sometimes it go to new page but it don't show error message.
I think is a rule in Rails. But I still want to ask that anyone have any idea to resolve this problem??? Go tonewlink witherror messageif they're failed
More details: I'm using Typus gem to create view for admin. So I can't find Routes file. I run rake routes and get:
GET /admin/books/(:/action(/:id)) (.:format)
POST /admin/books/(:/action(/:id)) (.:format)
PATCH /admin/books/(:/action(/:id)) (.:format)
DELETE /admin/books/(:/action(/:id)) (.:format)
And controller when create book:
if result
format.html { redirect_on_success }
format.json { render json: #item }
else
format.html { render :new, status: :unprocessable_entity }
format.json { render json: #item.errors, status: :unprocessable_entity }
end
Thanks for your helping :)
That is the normal way that Rails works. To understand what is happening, you need to understand what is HTTP verbs and how they works.
When you visit http://localhost:3000/book/new, you are making a request to the server to get (GET Verb) some information. In this case, a form to submit a new Book.
When You click submit, you are Sending (POST verb) data to the Server. On Rails, the link http://localhost:3000/book/create is available only by POST request. That is why, when you visit this link directly, it says that the route was not found.
This line:
# ...
else
format.html { render :new, status: :unprocessable_entity
end
means that, if something wrong happens, it need to render the view of new action again without redirect. This way you are able to find the errors on the object you are trying to save.
If you redirect, you will lose the actual (at the create stage) object. New object without data and error will be created on the new action:
def new
#book = Book.new
end
For this reason you are unable to access the error mensagens when you redirect. Only you can do on redirection, is setting a flash message:
if #book.save
redirect_to #book
else
flash[:error] = "An error occurred while saving Book."
redirect_to :new
end
2 resources that will hep you with that:
https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods
http://guides.rubyonrails.org/routing.html#crud-verbs-and-actions
On your rake routes, you could notice that it is prefixed with admin.
GET /admin/books/(:/action(/:id)) (.:format)
POST /admin/books/(:/action(/:id)) (.:format)
PATCH /admin/books/(:/action(/:id)) (.:format)
DELETE /admin/books/(:/action(/:id)) (.:format)
Did you try it to prefixed with admin/books/new? And admin/books/create? Then notice your url: you use only book since on your routes it is books.
Try:
http://localhost:3000/admin/books/new
http://localhost:3000/admin/books/create
You shouldn't be getting that error, there (by default) is no /create path, especially with a GET verb.
Whilst you can create your own /create path, your functionality is conventional:
#config/routes.rb
scope :admin do
resources :books, :people, only: [:new, :create] #-> url.com/admin/books/new
end
#app/controllers/books_controller.rb
class BooksController < ApplicationController
respond_to :json, :html, only: :create #-> needs responders gem
def new
#book = Book.new
end
def create
#book = Book.new book_params
respond_with #book if #book.save
end
end
The above is the standardized (working) way to achieve what you want.
--
As per the routes, there is no /create path:
POST /photos photos#create create a new photo

How do I stub a method response inside a larger method?

I do not yet have a good grasp on mocking and stubbing. How would I go about stubbing the aodc.success? call to return false? The AodcWrapper::Order.create is not complete and will always return success today. But I need to simulate the fail case.
Here is the create method
def create
#user = current_user
#order = Order.new(order_params)
if #order.valid?
aodc = AodcWrapper::Order.create(#order)
if aodc.success?
# pending... Capture the authorization
#order.save
UserMailer.order_received_email(#user).deliver_later
StaffMailer.order_received_email(#user).deliver_later
render json: #order, serializer: Api::V1::OrderSerializer
else
render json: { status: :failure, error: aodc.error_message }
end
else
render json: { status: :failure, error: #order.errors.full_messages }
end
end
And here is the test
context "sad path 1: can not connect to the aodc" do
before do
#user = FactoryGirl.create(:user)
#order = FactoryGirl.attributes_for(:shippable_order, user: #user)
sign_in #user
post :create, user_id: #user.id, order: #order
end
it "reponds with a could not connect message" do
parsed_response = JSON.parse(response.body)
expect(parsed_response[:error]).not_to be_nil
end
end
Side quest. Any recommendations on resources to explore so that I can not suck at mocks and stubs?
So I was going about that completely wrong. The AodcWrapper was making an API call with Httparty.
The path I took to solve this was
Use VCR to record the API interaction
The test failed because the response was successful
Modify the VCR cassette yml and change the success response to my desired (not yet implemented) error message
Rerun the test and all is well.

One action in Rails controller sends no data back

So I have these two actions in my RelationshipController:
class RelationshipsController < ApplicationController
before_filter :authenticate_user!
respond_to :json
def create
user = User.find(params[:user_to_follow_id])
relationship = current_user.follow!(user)
respond_with user
end
def destroy
user = User.find(params[:id])
current_user.unfollow!(user)
logger.debug user.id
respond_with user
end
end
Now when I use the create action, I get a response back with the respond_with user method which is also nicely formatted with ActiveModel::Serializer.
When I use the destroy method, I get a response back which is completely blank. It does seems to work with render json: {user: user, status: 201} but I am wondering why am I not able to use respond_with for both of them? The user they send back has the same type of data in both cases.
Unfollow method:
def unfollow!(user)
$redis.multi do
$redis.srem(self.redis_key(:following), user.id)
$redis.srem(user.redis_key(:followers), self.id)
end
end
Looks like you're bumping into the default response for action_controller / responder.rb
# File actionpack/lib/action_controller/metal/responder.rb, line 203
def api_behavior(error)
raise error unless resourceful?
raise MissingRenderer.new(format) unless has_renderer?
if get?
display resource
elsif post?
display resource, :status => :created, :location => api_location
else
head :no_content
end
end
If you're responding to a get or post, then some content is expected back, otherwise just a head and no content. By default, you just deleted a relationship, so there's nothing to return. If you want to send back a user in response to a relationship deletion, then I think you'll have to craft your own response (your render son statement).

ActionController::UnknownFormat: when using Rspec

I am creating a simple API to perform CRUD actions on contacts. :index, :show, :create and :update methods are working properly and they all pass their respective request rspec tests. The problem only arise in the :destroy method. The :destroy action looks like this:
class Api::V1::ContactsController < ApplicationController
def destroy
#contact = Contact.find(params[:id])
#contact.destroy
respond_to do |format|
format.json {render action: :index }
end
end
end
The spec to test DELETE request looks like this:
describe "DELETE /api/v1/contacts/:id" do
before(:each) do
#contact = FactoryGirl.create(:contact)
end
it "it should have a status code of 200" do
delete "/api/v1/contacts/#{#contact.id}"
expect(response.status).to eq(200)
end
it "It should delete a contact" do
expect{
delete "/api/v1/contacts/#{#contact.id}"
}.to change(Contact, :count).by(-1)
end
end
when i run the test i get this error message:
Failure/Error: delete "/api/v1/contacts/#{#contact.id}"
ActionController::UnknownFormat:
ActionController::UnknownFormat
# ./app/controllers/api/v1/contacts_controller.rb:67:in `destroy'
respond_to do |format| is line 67 in contacts_controller.rb
Try changing the format of your request to JSON, i.e. replace
delete "/api/v1/contacts/#{#contact.id}"
by
delete "/api/v1/contacts/#{#contact.id}.json"
that would allow the format logic to figure out that you want the reply rendered in JSON which it knows about.

How to redirect to a 404 in Rails?

I'd like to 'fake' a 404 page in Rails. In PHP, I would just send a header with the error code as such:
header("HTTP/1.0 404 Not Found");
How is that done with Rails?
Don't render 404 yourself, there's no reason to; Rails has this functionality built in already. If you want to show a 404 page, create a render_404 method (or not_found as I called it) in ApplicationController like this:
def not_found
raise ActionController::RoutingError.new('Not Found')
end
Rails also handles AbstractController::ActionNotFound, and ActiveRecord::RecordNotFound the same way.
This does two things better:
1) It uses Rails' built in rescue_from handler to render the 404 page, and
2) it interrupts the execution of your code, letting you do nice things like:
user = User.find_by_email(params[:email]) or not_found
user.do_something!
without having to write ugly conditional statements.
As a bonus, it's also super easy to handle in tests. For example, in an rspec integration test:
# RSpec 1
lambda {
visit '/something/you/want/to/404'
}.should raise_error(ActionController::RoutingError)
# RSpec 2+
expect {
get '/something/you/want/to/404'
}.to raise_error(ActionController::RoutingError)
And minitest:
assert_raises(ActionController::RoutingError) do
get '/something/you/want/to/404'
end
OR refer more info from Rails render 404 not found from a controller action
HTTP 404 Status
To return a 404 header, just use the :status option for the render method.
def action
# here the code
render :status => 404
end
If you want to render the standard 404 page you can extract the feature in a method.
def render_404
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
and call it in your action
def action
# here the code
render_404
end
If you want the action to render the error page and stop, simply use a return statement.
def action
render_404 and return if params[:something].blank?
# here the code that will never be executed
end
ActiveRecord and HTTP 404
Also remember that Rails rescues some ActiveRecord errors, such as the ActiveRecord::RecordNotFound displaying the 404 error page.
It means you don't need to rescue this action yourself
def show
user = User.find(params[:id])
end
User.find raises an ActiveRecord::RecordNotFound when the user doesn't exist. This is a very powerful feature. Look at the following code
def show
user = User.find_by_email(params[:email]) or raise("not found")
# ...
end
You can simplify it by delegating to Rails the check. Simply use the bang version.
def show
user = User.find_by_email!(params[:email])
# ...
end
The newly Selected answer submitted by Steven Soroka is close, but not complete. The test itself hides the fact that this is not returning a true 404 - it's returning a status of 200 - "success". The original answer was closer, but attempted to render the layout as if no failure had occurred. This fixes everything:
render :text => 'Not Found', :status => '404'
Here's a typical test set of mine for something I expect to return 404, using RSpec and Shoulda matchers:
describe "user view" do
before do
get :show, :id => 'nonsense'
end
it { should_not assign_to :user }
it { should respond_with :not_found }
it { should respond_with_content_type :html }
it { should_not render_template :show }
it { should_not render_with_layout }
it { should_not set_the_flash }
end
This healthy paranoia allowed me to spot the content-type mismatch when everything else looked peachy :) I check for all these elements: assigned variables, response code, response content type, template rendered, layout rendered, flash messages.
I'll skip the content type check on applications that are strictly html...sometimes. After all, "a skeptic checks ALL the drawers" :)
http://dilbert.com/strips/comic/1998-01-20/
FYI: I don't recommend testing for things that are happening in the controller, ie "should_raise". What you care about is the output. My tests above allowed me to try various solutions, and the tests remain the same whether the solution is raising an exception, special rendering, etc.
You could also use the render file:
render file: "#{Rails.root}/public/404.html", layout: false, status: 404
Where you can choose to use the layout or not.
Another option is to use the Exceptions to control it:
raise ActiveRecord::RecordNotFound, "Record not found."
The selected answer doesn't work in Rails 3.1+ as the error handler was moved to a middleware (see github issue).
Here's the solution I found which I'm pretty happy with.
In ApplicationController:
unless Rails.application.config.consider_all_requests_local
rescue_from Exception, with: :handle_exception
end
def not_found
raise ActionController::RoutingError.new('Not Found')
end
def handle_exception(exception=nil)
if exception
logger = Logger.new(STDOUT)
logger.debug "Exception Message: #{exception.message} \n"
logger.debug "Exception Class: #{exception.class} \n"
logger.debug "Exception Backtrace: \n"
logger.debug exception.backtrace.join("\n")
if [ActionController::RoutingError, ActionController::UnknownController, ActionController::UnknownAction].include?(exception.class)
return render_404
else
return render_500
end
end
end
def render_404
respond_to do |format|
format.html { render template: 'errors/not_found', layout: 'layouts/application', status: 404 }
format.all { render nothing: true, status: 404 }
end
end
def render_500
respond_to do |format|
format.html { render template: 'errors/internal_server_error', layout: 'layouts/application', status: 500 }
format.all { render nothing: true, status: 500}
end
end
and in application.rb:
config.after_initialize do |app|
app.routes.append{ match '*a', :to => 'application#not_found' } unless config.consider_all_requests_local
end
And in my resources (show, edit, update, delete):
#resource = Resource.find(params[:id]) or not_found
This could certainly be improved, but at least, I have different views for not_found and internal_error without overriding core Rails functions.
these will help you...
Application Controller
class ApplicationController < ActionController::Base
protect_from_forgery
unless Rails.application.config.consider_all_requests_local
rescue_from ActionController::RoutingError, ActionController::UnknownController, ::AbstractController::ActionNotFound, ActiveRecord::RecordNotFound, with: lambda { |exception| render_error 404, exception }
end
private
def render_error(status, exception)
Rails.logger.error status.to_s + " " + exception.message.to_s
Rails.logger.error exception.backtrace.join("\n")
respond_to do |format|
format.html { render template: "errors/error_#{status}",status: status }
format.all { render nothing: true, status: status }
end
end
end
Errors controller
class ErrorsController < ApplicationController
def error_404
#not_found_path = params[:not_found]
end
end
views/errors/error_404.html.haml
.site
.services-page
.error-template
%h1
Oops!
%h2
404 Not Found
.error-details
Sorry, an error has occured, Requested page not found!
You tried to access '#{#not_found_path}', which is not a valid page.
.error-actions
%a.button_simple_orange.btn.btn-primary.btn-lg{href: root_path}
%span.glyphicon.glyphicon-home
Take Me Home
routes.rb
get '*unmatched_route', to: 'main#not_found'
main_controller.rb
def not_found
render :file => "#{Rails.root}/public/404.html", :status => 404, :layout => false
end
<%= render file: 'public/404', status: 404, formats: [:html] %>
just add this to the page you want to render to the 404 error page and you are done.
I wanted to throw a 'normal' 404 for any logged in user that isn't an admin, so I ended up writing something like this in Rails 5:
class AdminController < ApplicationController
before_action :blackhole_admin
private
def blackhole_admin
return if current_user.admin?
raise ActionController::RoutingError, 'Not Found'
rescue ActionController::RoutingError
render file: "#{Rails.root}/public/404", layout: false, status: :not_found
end
end
Raising ActionController::RoutingError('not found') has always felt a little bit strange to me - in the case of an unauthenticated user, this error does not reflect reality - the route was found, the user is just not authenticated.
I happened across config.action_dispatch.rescue_responses and I think in some cases this is a more elegant solution to the stated problem:
# application.rb
config.action_dispatch.rescue_responses = {
'UnauthenticatedError' => :not_found
}
# my_controller.rb
before_action :verify_user_authentication
def verify_user_authentication
raise UnauthenticatedError if !user_authenticated?
end
What's nice about this approach is:
It hooks into the existing error handling middleware like a normal ActionController::RoutingError, but you get a more meaningful error message in dev environments
It will correctly set the status to whatever you specify in the rescue_responses hash (in this case 404 - not_found)
You don't have to write a not_found method that needs to be available everywhere.
To test the error handling, you can do something like this:
feature ErrorHandling do
before do
Rails.application.config.consider_all_requests_local = false
Rails.application.config.action_dispatch.show_exceptions = true
end
scenario 'renders not_found template' do
visit '/blah'
expect(page).to have_content "The page you were looking for doesn't exist."
end
end
If you want to handle different 404s in different ways, consider catching them in your controllers. This will allow you to do things like tracking the number of 404s generated by different user groups, have support interact with users to find out what went wrong / what part of the user experience might need tweaking, do A/B testing, etc.
I have here placed the base logic in ApplicationController, but it can also be placed in more specific controllers, to have special logic only for one controller.
The reason I am using an if with ENV['RESCUE_404'], is so I can test the raising of AR::RecordNotFound in isolation. In tests, I can set this ENV var to false, and my rescue_from would not fire. This way I can test the raising separate from the conditional 404 logic.
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :conditional_404_redirect if ENV['RESCUE_404']
private
def conditional_404_redirect
track_404(#current_user)
if #current_user.present?
redirect_to_user_home
else
redirect_to_front
end
end
end

Resources