Rails 3 TestUnit file fixture Tempfile becomes String in controller - ruby-on-rails

I have the following code to test file uploading:
test 'when a user adds an attachment to an existing candidate, the attachment shows up on the candidates page' do
user = login_user
opportunity = opportunities(:with_candidates)
candidate = candidates(:first)
upload = fixture_file_upload(ActionController::TestCase.fixture_path + 'files/file_upload_support_image.jpeg', 'image/jpeg')
attributes = { attachments: [upload] }
user.put opportunity_candidate_path(opportunity, candidate, request: attributes)
user.follow_redirect!
assert_match /#{upload.original_filename}/, fixer.response.body, 'The filename of the attachment should appear on the opportunity candidates page'
end
The test fails because the file does not get attached.
The code works when run through the browser – it turns out I had written the assert incorrectly and was getting a false pass. Now the assert is correct, the test fails.
When debugging, I find that the upload variable in the test is something like #<Rack::Test::UploadedFile:0x007f85ca141c50>. However, when I debug the controller, the value in the params hash is "#<Rack::Test::UploadedFile:0x007f85ca141c50>".
Note the quote marks. Rails has turned the file upload into a string!
Since there’s none of my code between the test and the controller, and this works when the app is run in the browser, I guess something may be wrong with how I am constructing the params hash or the tempfile itself in the test.
Any ideas what I am doing wrong?

Okay, turns out I was doing something incorrectly in the test. However, this error does not manifest if you are passing the most common types of parameter, so it can be a little tricky to spot!
The line
user.put opportunity_candidate_path(opportunity, candidate, request: attributes)
Should be
user.put opportunity_candidate_path(opportunity, candidate), request: attributes
Note the moved parenthesis. D’oh!
The reason this may not be easy to spot is that unknown keys in the ..._path method are still passed as parameters. However, they are parsed having been encoded into the URL querystring, like a GET request.
For the common cases of passing text values and integer foreign keys in as params, this mangling doesn’t prevent anything working, it’s only for more complex objects that it becomes an issue.

Related

Can't modify frozen String on Hash

I am a bit confused with the frozen string and utilizing them with test cases.
I just added the following line at the top of my test cases :
# frozen_string_literal: true
And i have the following two test cases:
test "Create upload invoice invalid invoice id" do
post :upload, params = {invoices_data: [{invoice_id: 987654, unit_id: 1321}]}
assert_response :not_found
end
test "Create upload invoice request to fortnox with non array request parameter" do
request = {invoices_data: {invoice_id: "invoice.id", unit_id: 321}}
post :upload_invoices, params = request
assert_response :bad_request
end
All of a sudden my second test failed with
RuntimeError: can't modify frozen String
at this line
post :upload_invoices, params = request
however, if I change the string to some symbol for instance :invoice_id then it works just fine.
Can someone guide why about the following two things:
Why does sending a string value fails in this case reporting that I
am trying to modify a String and which string value I am trying to
modify?
Why does it fail on post request, if it has to fail then it
should fail when creating the request i.e request = {invoices_data: {invoice_id: "invoice.id", unit_id: 321}}
What i can do to send string value instead of Symbol in the hash?
1a) Sending a string value fails in this case because your upload_invoices controller action attempts to modify the invoice_id parameter itself. (Or you're running an old version of Rails where the #post method itself attempts to modify the invoice_id parameter by converting it to UTF-8 encoding.)
1b) The string value you're trying to modify is "invoice.id".
2 ) It fails on the post request and not the assignment to the request variable because the assignment to the request variable is not where the attempted modification happens. The frozen string literal is attempted to be modified by the call to #post. See answer 1a above.
3 ) You can send a non-frozen string value in the hash a few different ways. You could remove the # frozen_string_literal: true magic comment, but I feel you don't want to do that. Otherwise, the simplest thing to do is to send along a duplicate of the string with either +'invoice.id' or the less esoteric 'invoice.id'.dup. Or you can create a non-literal string with something silly like ['invoice', 'id'].join('.') or :invoice.to_s. No doubt there are other ways.
However, it seems EXTREMELY unlikely you want to pass a string here at all. The invoice_id parameter is almost assuredly an integer, and passing a string to it makes little sense unless I guess you're trying to test that the controller action can handle that kind of erroneous input. If so, one of the string duplication techniques +'string_literal'/'string_literal'.dup would be your best option.
I would wager by the name of the test that you're actually trying to send along a real invoice_id which means you don't want to pass along a string, but instead an integer. Maybe the ID of an Invoice fixture you have setup?
And on another slightly unrelated note, you're not passing params to the #post method properly. It should be params: ... not params = ....

Why does RSpec/Capybara throw an error with hyphenated values provided with public_send?

I have a shared example I am using to test the same thing on several controllers. I pass the controller the model and a list of fields to check to make sure they are on the page, but when the value is a hyphenated string it throws an error, even when the value is on the page. The strange thing is, though, that it drops the hyphen in the check. So, if the last name field in the user table has "Johnson-Smith" This is what I get back...
expected to find text matching "Johnson Smith" in...
And looking down in the page text it renders I can clearly see "Johnson-Smith". I don't understand why Rspec/Capybara aren't checking for the correct value. Here is the shared example code..
RSpec.shared_examples 'renders proper fields' do |klass, fields|
it 'should render the expected text' do
model_object = klass.order(:created_at).last
fields&.each do |field|
expect(page).to have_text(model_object.public_send(field))
end
end
end
EDIT I tested the results for every public_send(field) that is evaluated and it turns out that public_send is returning the field from the database as expected. So the issue is, for some reason, Capybara is evaluating it without the hyphen, even though it passed the text with the hyphen.

Delayed Job object not properly deserialized

I'm having a hard time believing what I'm seeing, but it sure looks like DJ is failing to deserialize an object properly. I look at the DJ record in mongo and I see in the YAML that the object has its text field set, but when the code runs, the text field is not set. Here is some minimal repro code:
class Board
include Mongoid::Document
field :text, type: String
def process_text_field
if not self.text
raise "Text field is blank"
end
# Text field gets processed
end
end
# in a controller
def start_doing_something_slow
board = Board.find(params[:id])
board.text = "Text field is set"
board.save!
raise "Text disappeared!" unless board.text
board.delay.process_text_field
render json: {:result=>'ok'}
end
I invoke the controller method with the browser, and check the DJ record directly in mongo. I see in the YAML that the Board object has the text field correctly set. But when it executes in DJ, it raises the Text field is blank exception.
Somehow it's not deserializing the object properly.
Well this took me about a week to figure out, so I'm posting it here to help others who fall into this trap. Turns out this is a known bug in delayed_job_mongoid. And it's had a simple fix listed right there in the bug report for 10 months.
The problem arises if you use the identity map in mongoid, which acts as an in-process caching layer to the database. For normal web requests, the cache gets cleared between each request, so your controller methods don't use stale versions of the objects. But delayed_job_mongoid doesn't clear the cache between jobs without this patch (which I just put together): https://github.com/collectiveidea/delayed_job_mongoid/pull/38
The result is your delayed jobs are sometimes using old versions of the objects, depending on what ran before them, which creates truly bizarre, mysterious failures that are extremely difficult to track down until you understand what's happening.

Nested model error messages

I am using Ruby on Rails 3.0.9 and I am trying to validate a nested model. Supposing that I run validation for the "main" model and that generates some errors for the nested model I get the following:
#user.valid?
#user.errors.inspect
# => {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
How you can see the RoR framework creates an errors hash having following keys: account.firstname, account.lastname, account. Since I would like to display error messages on the front-end content by handling those error key\value pairs with JavaScript (BTW: I use jQuery) that involves CSS properties I thought to "prepare" that data and to change those keys to account_firstname, account_lastname, account (note: I substitute the . with the _ character).
How can I change key values from, for example, account.firstname to account_firstname?
And, mostly important, how I should handle this situation? Is what I am trying to do a "good" way to handle nested model errors? If no, what is the common\best approach to do that?
I've made a quick Concern which shows full error messages for nested models:
https://gist.github.com/4710856
#1.9.3-p362 :008 > s.all_full_error_messages
# => ["Purchaser can't be blank", "Consumer email can't be blank", "Consumer email is invalid", "Consumer full name can't be blank"]
Some creative patching of the Rails errors hash will let you achieve your aim. Create an initializer in config/initalizers, let call it errors_hash_patch.rb and put the following in it:
ActiveModel::Errors.class_eval do
def [](attribute)
attribute = attribute.to_sym
dotted_attribute = attribute.to_s.gsub("_", ".").to_sym
attribute_result = get(attribute)
dotted_attribute_result = get(dotted_attribute)
if attribute_result
attribute_result
elsif dotted_attribute_result
dotted_attribute_result
else
set(attribute, [])
end
end
end
All you're doing in here is simply overriding the accessor method [] to try a little harder. More specifically, if the key you're looking for has underscores, it will try to look it up as is, but if it can't find anything it will also replace all the underscores with dots and try to look that up as well. Other than that the behaviour is the same as the regular [] method. For example, let's say you have an errors hash like the one from your example:
errors = {:"account.firstname"=>["is too short", "can not be blank"], :"account.lastname"=>["is too short", "can not be blank"], :account=>["is invalid"]}
Here are some of the ways you can access it and the results that come back:
errors[:account] => ["is invalid"]
errors[:"account.lastname"] => ["is too short", "can not be blank"]
errors[:account_lastname] => ["is too short", "can not be blank"]
errors[:blah] => []
We don't change the way the keys are stored in the errors hash, so we won't accidentally break libraries and behaviours that may rely on the format of the the hash. All we're doing is being a little smarter regarding how we access the data in the hash. Of course, if you DO want to change the data in the hash, the pattern is the same you will just need to override the []= method, and every time rails tries to store keys with dots in them, just change the dots to underscores.
As to your second question, even though I have shown you how to do what you're asking, in general it is best to try and comply with the way rails tries to do things, rather than trying to bend rails to your will. In your case, if you want to display the error messages via javascript, presumably your javascript will have access to a hash of error data, so why not tweak this data with javascript to be in the format that you need it to be. Alternatively you may clone the error data inside a controller and tweak it there (before your javascript ever has access to it). It is difficult to give advice without knowing more about your situation (how are you writing your forms, what exactly is your validation JS trying to do etc.), but those are some general guidelines.
I had the same problem with AngularJs, so I decided to overwrite the as_json method for the ActiveModel::Errors class in an initializer called active_model_errors.rb so that it can replace . for _
Here is the initializer code:
module ActiveModel
class Errors
def as_json(options=nil)
hash = {}
to_hash(options && options[:full_messages]).each{ |k,v| hash[k.to_s.sub('.', '_')] = messages[k] }
hash
end
end
end
I hope it can be helpful for someone
I'm not sure but I think you can't change that behavior without pain. But you could give a try to solutions like http://bcardarella.com/post/4211204716/client-side-validations-3-0

Ruby on Rails 2.3.8: Functional Testing: Numbers not being passed as string in params?

Note * I'm using Factory_girl, not sure if that matters here
Currently, I do the following to prep for doing a post:
p = Factory.build(:le_object, :template_id => t.id.to_s)
Notice that I am deliberately converting it to a string
And then I just do your ordinary post:
post :create, :le_object => p.attributes
But, what is fishy, is when I look in the test log, I see this:
Parameters: {"action"=>"create", ... "template_id"=>0, ... }
Notice how the id is NOT a string.
Now, this messes up the controller, because I do a not .empty? check on that field (because it is optional), and then that method in the controller does something entirely different.
This works fine in the app, when it is actually running, as for the same method, the console outputs this in dev / production:
Parameters: {"action"=>"create", ... "template_id"=>"1"...}
Notice how here the 1 is a string, because it came from a form, and in HTTP, everything is strings.
This is what in my code is failing, specifically:
if not params[:le_object][:template_id].nil? and not params[:le_object][:template_id].empty?
The issue currently, is that in the test, because it's passing a number, instead of the string representation, I'm getting the error undefined method empty? for #:Fixnum
So... How do I change my code (test or otherwise) such that I can get this to work?
Rails defines the .blank? method on Object, meaning it works on just about anything (strings, numbers, etc.) The definition of .blank? is very simple:
respond_to?(:empty?) ? empty? : !self
It will default to use empty?, but if that method doesn't exist on the object, it will just call !self. If the item is nil, then !nil will evaluate to true.

Resources