One of the tests in a scaffold-generated RSpec controller spec fails, and it looks to me as if it must always fail by design, but of course it is surely supposed to succeed.
I develop a Rails 4 app with RSpec specs generated by rails g scaffold.
The controller spec for my SkillsController requires me to fill in a 'valid attributes' hash and an 'invalid attributes' hash for my model, which I did.
The tests all succeed except for "PUT update with invalid params re-render the 'edit' template":
1) SkillsController PUT update with invalid params re-renders the 'edit' template
Failure/Error: expect(response).to render_template("edit")
expecting <"edit"> but rendering with <[]>
# ./spec/controllers/skills_controller_spec.rb:139:in `block (4 levels) in <top (required)>'
In the Rails console, I confirmed that my invalid_params hash contains invalid parameters ({ hack: 'hack' }).
The controller calls the skill_params method which returns an empty hash because my invalid_params hash contains only invalid parameters.
Calling skill.update(skill_params) with an empty skill_params hash returns true, so that the else part will never execute, and the 'new' template will not be rendered:
def update
respond_to do |format|
if #skill.update(skill_params) # empty hash due to invalid params!
format.html { redirect_to #skill, notice: 'Skill was successfully updated.' }
format.json { render :show, status: :ok, location: #skill }
else
format.html { render :edit }
format.json { render json: #skill.errors, status: :unprocessable_entity }
end
end
end
To summarize: The spec PUTs a hash with invalid parameters to my SkillController. The SkillController's 'skill_params' cleans up this hash, returning an empty hash. skill.update with an empty hash is a no-op (confirmed on the console), and the method returns true.
Therefore, the assertion that the 'edit' template should be rendered will never, ever be true, and the default controller spec for the update action with invalid params will never turn green.
What am I missing here?
I finally figured it out and post the solution here, in case someone else finds himself/herself in the same situation.
The 'invalid params' hash is not so much about parameters with invalid attribute names, it's more about invalid attribute values.
Invalid attribute names are simply ignored/discarded, just as #Simone Carletti pointed out in his response.
If invalid values are PUT to the controller, the mymodel.update method will return false, and the controller redirects to the :edit action.
Thus, in order to make the generated spec pass, the 'invalid attributes' hash at the top of the spec file should be filled with valid attribute names and invalid attribute values (e.g., nil, if an attribute is required).
This is what you should expect. When you use the strong parameters, the returned Hash only contains the attributes you explicitly permit.
This means that if you don't allow the hack parameter, then if you usmbit a request with
{ hack: 'hack' }
the results of skill_params will be
{}
By design, update is perfectly happy to accept an empty Hash. Therefore, if you execute
#skill.update({})
the result is true because the execution is successful. Of course, it didn't do anything, but it didn't fail.
Therefore, your controller will silently succeed and it will not run the edit condition.
I don't see why you would expect that passing a "not handled" param should cause the controller to show the edit page. It happens every day. Just take a random site and append random query strings to it. For example, go to
http://stackoverflow.com/?tab=interesting
and append a random query string
http://stackoverflow.com/?tab=interesting&foo=bar
The page will happily ignore it. It will not render an error, nor it will crash. The same happens to your controller, the unknown params such as hack as silently ignored and the controller renders the successful action.
Related
Basically I have the following controller method:
def create
begin
#check_in = create_check_in()
rescue => exception
render json: { message: exception }, status: 500
end
end
and the following json.rabl file:
object #check_in => :event_check_in
attributes :id
What I try to achieve is to set manually the HTTP status code of the response. It currently responds with 200, and I need it to be 201 instead.
I saw very few similar question and the answer was generally to render / respond_with from the controller action, so I tried something like this:
def create
begin
#check_in = create_check_in()
render #check_in, status: 201
rescue => exception
render json: { message: exception }, status: 500
end
end
but all my attempts failed, throwing various errors.
Is there a way I could set the status code?
The issue is you're passing in #check_in as the first argument of the render method when it expects the first argument to be a hash of options, including the status option.
Your status: 201 option is being passed in as a hash to the methods second argument and being ignored.
Typicallya render call will look something like:
render json: #check_in.as_json, status: 201
# or more commonly something like
render action: :create, status: 201 # the #check_in variable is already accessible to the view and doesn't need to be passed in
# or just
render status: 201
# since by default it will render the view with the same name as the action - which is `create`.
There are lots of ways to call render, see the docs for more.
-- EDIT --
Max has a great comment - I'd strongly advise against rescuing from all exceptions and also against doing it in a specific controller action. In addition to his suggestion, Rails 5+ supports :api formatting for exceptions out of the box or, if you need more, I'd look at a guide like this one.
I am trying to run a command on render so have added the below code. It works most of the time except on a few pages I get the error:
wrong number of arguments (given 1, expected 0)
This is the code it is failing on:
def render
record_current_admin_timestamp
super
end
Pages that this seems to happen on are places like sign in forms and when forms fail submission. We are using unchanged devise for sign in and edit forms for admins and those are the main ones that seem to fail.
Here is the code again that is where the issue is occurring. It stops at the beginning of render and doesn't even enter the method if I put a pry or raise in there.
application_controller:
def render
record_current_admin_timestamp
super
end
def record_current_admin_timestamp
PageVisit.create(admin: current_admin, url: request.url)
end
This is the type of code when it fails on the render edit. All my rspec tests that previously passed now fail when it hits the render :edit line.
def update
return not_found_redirect(projects_path) unless project
return redirect_success if project.update_attributes(params_project)
flash[:error] = record_not_saved
render :edit
end
Any ideas would be appreciated as I am at a loss now. If you need any more info or code let me know.
You call render :edit in your update method. That means your overridden render needs to accept at least one argument, but def render does not.
In your example, it probably makes sense that you just accept all arguments and pass them to the super call. That would lead your render method to not raise errors but super would if it is called with an incompatible number of arguments.
def render(*args)
record_current_admin_timestamp
super
end
I know there are several SO questions as well as online articles on using yield in Rails. But I'm still having trouble understanding what's wrong with my code below, and would appreciate any advice.
In my app, I have:
A controller that passes data to the command class's run method, and returns the request status based on the result of the Command.run (true/false)
A command class that deals with the actual meat of the process, then yields true if it succeeded, or false if it failed
However, the command class seems to be failing to yield the results to my controller. According to the error messages when I run my tests, it seems like my block in the controller isn't being recognized as a block:
# If I use "yield result":
LocalJumpError: no block given (yield)
# If I use "yield result if block_given?":
# (This is because I have "assert_response :success" in my tests)
Expected response to be a <2XX: success>, but was a <400: Bad Request>
How should I rewrite the block (do ... end part in the controller below) so that yield works correctly? Or if the issue lies elsewhere, what am I doing wrong?
I've provided a simplified version of my code below. Thank you in advance!
# controller
def create
Command.run(params) do
render json: { message: 'Successfully processed request' }
return
end
render json: { message: 'Encountered an error' }, status: :bad_request
end
# command class
def run(params)
# Do some stuff, then send HTTP request
# "result" below returns true or false
result = send_http_request.parsed_response == 'ok'
yield result
end
def self.run(params)
new.run(params)
end
Note: This code works if I use if true... else... in the controller instead of a block, and just return the boolean result instead of yielding it. But here I'd like to know how to make yield work.
In your controller you need to have a variable for the result.
def create
Command.run(params) do |result|
if result
render json: { message: 'Successfully processed request' }, status: :success
else
render json: { message: 'Encountered an error' }, status: :bad_request
end
return
end
render json: { message: 'Encountered an error' }, status: :bad_request
end
(EDIT)
Also, you are calling the class method which call the instance method. You have to pass the block from the calling code to the instance method you are calling.
def self.run(params, &block)
new.run(params, &block)
end
EDIT: ah, so you have a class method run and instance method run.
Either do as Marlin has suggested and supply the block explicitly from class method to the instance method.
Or use only the class method as I've initially suggested (it doesn't
seem like there's any reason to instantiate Command in your case):
def self.run(params, &block)
result = send_http_request.parsed_response == 'ok'
block.yield(result)
end
So I'm creating a system where a visitor can ask my client for a music composition. The user starts to fill a form with details and such, and when he submits it, it's not yet sent to my client. However, he can get an estimation of the price, and modify his demand if he wants to. After that, He still can submit it one last time, and this time, the estimation is sent.
I don't want to use the default id as a parameter because it would be way too simple to 'parse' other estimations if the url looks ends with /3 or /4. You'd just have to try a few URLs and if it's your lucky day, you'd get to "hack" an estimation that isn't yours. I'm planning to use a cron job to delete these estimations after a while, but I don't want to take any risk.
To avoid that, I decided to use the visitor's session_id as a parameter, on which I removed every alphabetic characters, but still saved as a string in my MySQL 5.7 database so that ActiveRecord would be ok with that. I also changed my routes accordingly, and the result is supposed to be something like
localhost:3000/devis/4724565224204064191099 # Devis means 'quotation' in french
However, when I try to get to this route, I get the following error:
ActiveRecord::RecordNotFound (Couldn't find Devi with an out of range value for 'id')
Here is the relevant part of my controller:
devis_controller.rb
# ...
def create
#devi = Devi.new(devi_params)
respond_to do |format|
#devi.status = 'created'
#devi.session_id = session.id.gsub(/[^\d]/, '').to_s # How I store my parameter
# Latest record returns '4724565224204064191099'
if #devi.save
format.html { redirect_to #devi, notice: 'Devi was successfully created.' }
format.json { render :show, status: :created, location: #devi }
else
format.html { render :new }
format.json { render json: #devi.errors, status: :unprocessable_entity }
end
end
end
# ...
private
def set_devi
#devi = Devi.find(params[:session_id].to_s) # Tried '.to_s', didn't work
end
And here are my routes:
# 'index' and 'destroy' don't exist
resources :devis, only: %i(create update)
get '/devis/nouveau', to: 'devis#new', as: :devis_new
get '/devis/:session_id', to: 'devis#show', as: :devis_show
get '/devis/editer/:session_id', to: 'devis#edit', as: :devis_edit
My question is the following: is there a way to present the :session_id as a string to my controller's params? Or do I have a better option?
Thank you
I think session_id is a int at database, and then is where you should do the change.
change the type of session_id to string and ActiveRecord map session_id as string from then on
Utilizing ActionController's new respond_with method...how does it determine what to render when action (save) is successful and when it's not?
I ask because I'm trying to get a scaffold generated spec (included below) to pass, if only so that I can understand it. The app is working fine but, oddly, it appears to be rendering /carriers (at least that's what the browser's URL says) when a validation fails. Yet, the spec is expecting "new" (and so am I, for that matter) but instead is receiving <"">. If I change the spec to expect "" it still fails.
When it renders /carriers that page shows the error_messages next to the fields that failed validation as one would expect.
Can anyone familiar with respond_with see what's happening here?
#carrier.rb
validates :name, :presence => true
#carriers_controller.rb
class CarriersController < ApplicationController
respond_to :html, :json
...
def new
respond_with(#carrier = Carrier.new)
end
def create
#carrier = Carrier.new(params[:carrier])
flash[:success] = 'Carrier was successfully created.' if #carrier.save
respond_with(#carrier)
end
Spec that's failing:
#carriers_controller_spec.rb
require 'spec_helper'
describe CarriersController do
def mock_carrier(stubs={})
(#mock_carrier ||= mock_model(Carrier).as_null_object).tap do |carrier|
carrier.stub(stubs) unless stubs.empty?
end
end
describe "POST create" do
describe "with invalid params" do
it "re-renders the 'new' template" do
Carrier.stub(:new) { mock_carrier(:save => false) }
post :create, :carrier => {}
response.should render_template("new")
end
end
end
end
with this error:
1) CarriersController POST create with invalid params re-renders the 'new' template
Failure/Error: response.should render_template("new")
expecting <"new"> but rendering with <"">.
Expected block to return true value.
# (eval):2:in `assert_block'
# ./spec/controllers/carriers_controller_spec.rb:81:in `block (4 levels) in <top (required)>'
tl:dr
Add an error hash to the mock:
Carrier.stub(:new) { mock_carrier(:save => false,
:errors => { :anything => "any value (even nil)" })}
This will trigger the desired behavior in respond_with.
What is going on here
Add this after the post :create
response.code.should == "200"
It fails with expected: "200", got: "302". So it is redirecting instead of rendering the new template when it shouldn't. Where is it going? Give it a path we know will fail:
response.should redirect_to("/")
Now it fails with Expected response to be a redirect to <http://test.host/> but was a redirect to <http://test.host/carriers/1001>
The spec is supposed to pass by rendering the new template, which is the normal course of events after the save on the mock Carrier object returns false. Instead respond_with ends up redirecting to show_carrier_path. Which is just plain wrong. But why?
After some digging in the source code, it seems that the controller tries to render 'carriers/create'. There is no such template, so an exception is raised. The rescue block determines the request is a POST and there is nothing in the error hash, upon which the controller redirects to the default resource, which is the mock Carrier.
That is puzzling, since the controller should not assume there is a valid model instance. This is a create after all. At this point I can only surmise that the test environment is somehow taking shortcuts.
So the workaround is to provide a fake error hash. Normally something would be in the hash after save fails, so that kinda makes sense.