How to use include? in Rails - ruby-on-rails

hopefully a trivial question for the masters at SO. How would I use ruby's include? to see if Post's Comment owner includes the current_user? For example, this code works fine, but I'm trying to find the include? method equivalent to test if the User is included. Many thanks in advance!
# Brief has many Submissions
brief = Brief.first
user = User.first
brief.submissions.where('submissions.user': user) # works as expected and retrieves the correct Submission
brief.submissions.include?(user: user) # false but should return true

The result of where is an ActiveRecord_Relation, where every element inside is an instance of the same class the receiver is defined as, meaning every element in brief.submissions.where('submissions.user': user) is a Submission instance
In order for include? to return true, you must pass an object that's an instance of Submission, what you're passing right now is a Hash.
To solve that, try instead brief.submissions.include?(user).
But, if your query does what it means to, then you're not getting users as results, but submissions, filtering by the user to which they belong to. So, in any case, no matter what user you pass, it's going to return false.
I recommend you to use exists? if you're expecting to receive just a predicate value:
brief.submissions.exists?('submissions.user': user)

Related

Is there a way I can force a record to not be destroyed when running a feature test in RSpec? (Rails 6)

For context, I have a controller method called delete_cars. Inside of the method, I call destroy_all on an ActiveRecord::Collection of Cars. Below the destroy_all, I call another method, get_car_nums_not_deleted_from_portal, which looks like the following:
def get_car_nums_not_deleted_from_portal(cars_to_be_deleted)
reloaded_cars = cars_to_be_deleted.reload
car_nums = reloaded_cars.car_numbers
if reloaded_cars.any?
puts "Something went wrong. The following cars were not deleted from the portal: #{car_nums.join(', ')}"
end
car_nums
end
Here, I check to see if any cars were not deleted during the destroy_all transaction. If there are any, I just add a puts message. I also return the ActiveRecord::Collection whether there are any records or not, so the code to follow can handle it.
The goal with one of my feature tests is to mimic a user trying to delete three selected cars, but one fails to be deleted. When this scenario occurs, I display a specific notice on the page stating:
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{some_car_numbers_go_here}"
How can I force just one record to fail when my code executes the destroy_all, WITHOUT adding extra code to my Car model (in the form of a before_destroy or something similar)? I've tried using a spy, but the issue is, when it's created, it's not a real record in the DB, so my query:
cars_to_be_deleted = Car.where(id: params[:car_ids].split(',').collect { |id| id.to_i })
doesn't include it.
For even more context, here's the test code:
context 'when at least one car is not deleted, but the rest are' do
it "should display a message stating 'Some selected cars have been successfully...' and list out the cars that were not deleted" do
expect(Car.count).to eq(100)
visit bulk_edit_cars_path
select(#location.name.upcase, from: 'Location')
select(#track.name.upcase, from: 'Track')
click_button("Search".upcase)
find_field("cars_to_edit[#{Car.first.id}]").click
find_field("cars_to_edit[#{Car.second.id}]").click
find_field("cars_to_edit[#{Car.third.id}]").click
click_button('Delete cars')
cars_to_be_deleted = Car.where(id: Car.first(3).map(&:id)).ids
click_button('Yes')
expect(page).to have_text(
'Some selected cars have been successfully deleted from the portal, however, some have not. The '\
"following cars have not been deleted from the portal:\n\n#{#first_three_cars_car_numbers[0]}".upcase
)
expect(Car.count).to eq(98)
expect(Car.where(id: cars_to_be_deleted).length).to eq(1)
end
end
Any help with this would be greatly appreciated! It's becoming quite frustrating lol.
One way to "mock" not deleting a record for a test could be to use the block version of .to receive to return a falsy value.
The argument for the block is the instance of the record that would be :destroyed.
Since we have this instance, we can check for an arbitrary record to be "not destroyed" and have the block return nil, which would indicate a "failure" from the :destroy method.
In this example, we check for the record of the first Car record in the database and return nil if it is.
If it is not the first record, we use the :delete method, as to not cause an infinite loop in the test (the test would keep calling the mock :destroy).
allow_any_instance_of(Car).to receive(:destroy) { |car|
# use car.delete to prevent infinite loop with the mocked :destroy method
if car.id != Car.first.id
car.delete
end
# this will return `nil`, which means failure from the :destroy method
}
You could create a method that accepts a list of records and decide which one you want to :destroy for more accurate testing!
I am sure there are other ways to work around this, but this is the best we have found so far :)
If there is a specific reason why the deletion might fail you can simulate that case.
Say you have a RaceResult record that must always refer to a valid Car and you have a DB constraint enforcing this (in Postgres: ON DELETE RESTRICT). You could write a test that creates the RaceResult records for some of your Car records:
it 'Cars prevented from deletion are reported` do
...
do_not_delete_cars = Car.where(id: Car.first(3).map(&:id)).ids
do_not_delete_cars.each { |car| RaceResult.create(car: car, ...) }
click_button('Yes')
expect(page).to have_text(...
end
Another option would be to use some knowledge of how your controller interacts with the model:
allow(Car).to receive(:destroy_list_of_cars).with(1,2,3).and_return(false) # or whatever your method would return
This would not actually run the destroy_list_of_cars method, so all the records would still be there in the DB. Then you can expect error messages for each of your selected records.
Or since destroy_all calls each record's destroy method, you could mock that method:
allow_any_instance_of('Car').to receive(:destroy).and_return(false) # simulates a callback halting things
allow_any_instance_of makes tests brittle however.
Finally, you could consider just not anticipating problems before they exist (maybe you don't even need the bulk delete page to be this helpful?). If your users see a more generic error, is there a page they could filter to verify for themselves what might still be there? (there's a lot of factors to consider here, it depends on the importance of the feature to the business and what sort of things could go wrong if the data is inconsistent).

Rspec expect to receive not working for object in array

I'm sure this is answered somewhere; I can't seem to phrase my google search right though. I'm trying to test that a method is called on an object, but the method isn't called on the specific object in the spec. The method is called on the last item in a collection, which I've confirmed is the same underlying object as the one in the spec. I'm not sure how clear that was, so here is an example:
expect(#email).to receive(:send) # fails
puts #user.emails.last == #email # true
#user.emails.last.send
As a sanity check, this spec passes. However the code I'm testing has #user.emails.last.send in it, so I'm trying to figure out how to make the spec above pass.
expect(#email).to receive(:send) # passes
#email.send
Edit:
#user.emails.last.equal?(#email) returns false, so as suspected by #spickermann
and #Grzegorz the #user.emails.last and #email are two instances of the same object. So I guess what I'm asking is how can I test that the send method was called on a specific object (ignoring what particular instance of that object it was called on). My question is actually the same as this one that I just found Rspec: Test if instance method was called on specific record with ActiveRecord.
It's possible that == method is defined on the mail object in a way that it returns true if some attributes are the same, but it doesn't care if the object is the same.
#user.emails.last == #email
This is the case with a simple string:
>> "d" == "d"
=> true
>> "d".object_id == "d".object_id
=> false
So It is possible that #mail and #user.emails.last are different objects in memory, but return true when using == method.
You can confirm that there's nothing wrong with your expectation like this:
expect(#user.emails.last).to receive(:send) # should pass now
#user.emails.last.send
You didn't share much code for context, so it's not clear what a "good" solution in your case could be. But I hope this will point you in the right direction.
In my case I'm able to work around this by returning the Email instance (#user.emails.last) from the send_email method and ensuring that is the same object as the #email object in the spec. E.g.,:
# The `send_email` method calls `#user.emails.last.send` and returns `#user.emails.last`
email = #user.send_email
expect(email).to eq(#email) # passes!

How to perform #find on an object without Active Model

Here's what's happend: I have gone and pulled a large amount of data from an API. This is nice, but it includes a lot of results.
When I do a result.find(id: api_id) I get all the results like find was never performed. #where does not work either. I'm assuming this is because its not extending from Active Model.
Key Question: How do I find, say, the name of a particular object in an active resource collection?
Object.find(id: api_id) in active resource is essentially doing an api request as in (uri_of_api)/objects/:api_id)
But the :find method on an array is a different aninmal. You can look up the array 'find' method here... http://www.ruby-doc.org/core-2.1.1/Enumerable.html#method-i-find
The correct format would be...
result.find{|rec| rec.id == api_id}

Ruby array include? method weird behaviour

I'm using the cancan gem, and in ability.rb, I wanted to check if a user with a certain role can update another object if and only if the user 'is part of' the other object (that is, there is an association between the models, and the user is found in the object's users method).
I wanted to use the include? method to see if #current_user is found in the resulting User array.
It always returns false when testing the code through ability.rb. If I run the same code in the rails console, it works fine.
I found out that include? does not work if the Array objects are not of the same class as the passed object, but I printed their class through class.name, and they are the same.
I ended up using detect and comparing id's, but I wanted to know if anyone has had this kind of issue before, and if you could shed some light on this.
Something similar happened with delete_if, where after deleting 20 out of a 100, count was still returning 100, but an each loop printed only the ones that were supposed to be there (20). This happened when executing within the RoR environment, but the rails console with the same code behaved as expected (array.count # => 20).
EDIT 20131007
A bit of code regarding the include? issue.
This bit returns false all the time, even though the user IS in the resulting array from course.institution.users.
can [:read, :update], Course do |course|
val = course.institution.users.include? user
end
If I take that same line, and try it for a given course and user in the rails console, like so:
Course.find(course_id).institution.users.include? User.find(user_id)
It works as it's supposed to, returning true then the user is found in the array and false if not.
So even, even if the == operator was weird in some way for this particular model, I'm dealing with the same arrays in both cases, so it should either bust or work well in both cases, not only within the cancan ability.rb... right?
Cancan has a built-in mechanism to query associations. Without knowing your exact setup, this should be close to how you could set this up in your Ability class:
can :view, MyObject, users: { id: user.id }

Ruby on Rails - ActiveRecord::Relation count method is wrong?

I'm writing an application that allows users to send one another messages about an 'offer'.
I thought I'd save myself some work and use the Mailboxer gem.
I'm following a test driven development approach with RSpec. I'm writing a test that should ensure that only one Conversation is allowed per offer. An offer belongs_to two different users (the user that made the offer, and the user that received the offer).
Here is my failing test:
describe "after a message is sent to the same user twice" do
before do
2.times { sending_user.message_user_regarding_offer! offer, receiving_user, random_string }
end
specify { sending_user.mailbox.conversations.count.should == 1 }
end
So before the test runs a user sending_user sends a message to the receiving_user twice. The message_user_regarding_offer! looks like this:
def message_user_regarding_offer! offer, receiver, body
conversation = offer.conversation
if conversation.nil?
self.send_message(receiver, body, offer.conversation_subject)
else
self.reply_to_conversation(conversation, body)
# I put a binding.pry here to examine in console
end
offer.create_activity key: PublicActivityKeys.message_received, owner: self, recipient: receiver
end
On the first iteration in the test (when the first message is sent) the conversation variable is nil therefore a message is sent and a conversation is created between the two users.
On the second iteration the conversation created in the first iteration is returned and the user replies to that conversation, but a new conversation isn't created.
This all works, but the test fails and I cannot understand why!
When I place a pry binding in the code in the location specified above I can examine what is going on... now riddle me this:
self.mailbox.conversations[0] returns a Conversation instance
self.mailbox.conversations[1] returns nil
self.mailbox.conversations clearly shows a collection containing ONE object.
self.mailbox.conversations.count returns 2?!
What is going on there? the count method is incorrect and my test is failing...
What am I missing? Or is this a bug?!
EDIT
offer.conversation looks like this:
def conversation
Conversation.where({subject: conversation_subject}).last
end
and offer.conversation_subject:
def conversation_subject
"offer-#{self.id}"
end
EDIT 2 - Showing the first and second iteration in pry
Also...
Conversation.all.count returns 1!
and:
Conversation.all == self.mailbox.conversations returns true
and
Conversation.all.count == self.mailbox.conversations.count returns false
How can that be if the arrays are equal? I don't know what's going on here, blown hours on this now. Think it's a bug?!
EDIT 3
From the source of the Mailboxer gem...
def conversations(options = {})
conv = Conversation.participant(#messageable)
if options[:mailbox_type].present?
case options[:mailbox_type]
when 'inbox'
conv = Conversation.inbox(#messageable)
when 'sentbox'
conv = Conversation.sentbox(#messageable)
when 'trash'
conv = Conversation.trash(#messageable)
when 'not_trash'
conv = Conversation.not_trash(#messageable)
end
end
if (options.has_key?(:read) && options[:read]==false) || (options.has_key?(:unread) && options[:unread]==true)
conv = conv.unread(#messageable)
end
conv
end
The reply_to_convesation code is available here -> http://rubydoc.info/gems/mailboxer/frames.
Just can't see what I'm doing wrong! Might rework my tests to get around this. Or ditch the gem and write my own.
see this Rails 3: Difference between Relation.count and Relation.all.count
In short Rails ignores the select columns (if more than one) when you apply count to the query. This is because
SQL's COUNT allows only one or less columns as parameters.
From Mailbox code
scope :participant, lambda {|participant|
select('DISTINCT conversations.*').
where('notifications.type'=> Message.name).
order("conversations.updated_at DESC").
joins(:receipts).merge(Receipt.recipient(participant))
}
self.mailbox.conversations.count ignores the select('DISTINCT conversations.*') and counts the join table with receipts, essentially counting number of receipts with duplicate conversations in it.
On the other hand, self.mailbox.conversations.all.count first gets the records applying the select, which gets unique conversations and then counts it.
self.mailbox.conversations.all == self.mailbox.conversations since both of them query the db with the select.
To solve your problem you can use sending_user.mailbox.conversations.all.count or sending_user.mailbox.conversations.group('conversations.id').length
I have tended to use the size method in my code. As per the ActiveRecord code, size will use a cached count if available and also returns the correct number when models have been created through relations and have not yet been saved.
# File activerecord/lib/active_record/relation.rb, line 228
def size
loaded? ? #records.length : count
end
There is a blog on this here.
In Ruby, #length and #size are synonyms and both do the same thing: they tell you how many elements are in an array or hash. Technically #length is the method and #size is an alias to it.
In ActiveRecord, there are several ways to find out how many records are in an association, and there are some subtle differences in how they work.
post.comments.count - Determine the number of elements with an SQL COUNT query. You can also specify conditions to count only a subset of the associated elements (e.g. :conditions => {:author_name => "josh"}). If you set up a counter cache on the association, #count will return that cached value instead of executing a new query.
post.comments.length - This always loads the contents of the association into memory, then returns the number of elements loaded. Note that this won't force an update if the association had been previously loaded and then new comments were created through another way (e.g. Comment.create(...) instead of post.comments.create(...)).
post.comments.size - This works as a combination of the two previous options. If the collection has already been loaded, it will return its length just like calling #length. If it hasn't been loaded yet, it's like calling #count.
It is also worth mentioning to be careful if you are not creating models through associations, as the related model will not necessarily have those instances in its association proxy/collection.
# do this
mailbox.conversations.build(attrs)
# or this
mailbox.conversations << Conversation.new(attrs)
# or this
mailbox.conversations.create(attrs)
# or this
mailbox.conversations.create!(attrs)
# NOT this
Conversation.new(mailbox_id: some_id, ....)
I don't know if this explains what's going on, but the ActiveRecord count method queries the database for the number of records stored. The length of the Relation could be different, as discussed in http://archive.railsforum.com/viewtopic.php?id=6255, although in that example, the number of records in the database was less than the number of items in the Rails data structure.
Try
self.mailbox.conversations.reload; self.mailbox.conversations.count
or perhaps
self.mailbox.reload; self.mailbox.conversations.count
or, if neither of those work, just try reloading as many of the objects as possible to see if you can get it to work (self, mailbox, conversations, etc.).
My guess is that something is messed up between memory and the DB. This is definitely a really weird error though, might wanna put in an issue on Rails to see why this would be the case.
The result of mailbox.conversations is cached after the first call. To reload it write mailbox.conversations(true)

Resources