fabricated models not the same as on disk - ruby-on-rails

I'm probably misunderstanding something here.
I have a model Secondant that I create with Fabrication in my model rspec.
main_user = Fabricate :user, email: TESTEMAIL
sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
sec_user = Fabricate :user, email: SECEMAIL
ActionMailer::Base.deliveries = []
debugger
At this point when I look at the value of secondant_id in the sec model, the attribute is empty (it get's filled in a after_create callback). When i retrieve the model just created from the database that attribute is filled. Why are those two not in sync?
27 main_user = Fabricate :user, email: TESTEMAIL
28 sec = Fabricate :secondant, email: SECEMAIL, user_id: main_user.id
29 sec_user = Fabricate :user, email: SECEMAIL
30 ActionMailer::Base.deliveries = []
31 debugger
=> 32 sec.destroy
33 end
34
35 it 'should have a secondant_id assigned' do
36 sec.secondant_id.should_not be_nil
(rdb:1) e sec
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: nil, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
(rdb:1) e Secondant.where(id: sec.id).first
#<Secondant id: 519, user_id: 1095, email: "secondant#hotmail.com", secondant_id: 1096, created_at: "2013-10-10 13:13:29", updated_at: "2013-10-10 13:13:29", reported: false>
My after_create callback:
def find_user
user = User.where(email: self.email).first
if user
# create the link to the user
self.secondant_id = user.id
self.save
# see if this is the second one
if Secondant.where('user_id = ? and secondant_id is not null', user_id).count == 2
user.do_somthing
end
end
return
end
EDIT
There is a similar callback in the user class, which is firing in this case (thanks Peter)
def find_secondant
Secondant.where(email: email).find_each do |sec|
sec.secondant_id = id
sec.save
end
end

At the time you create sec, the user with the identical email has not been created, so your after_save callback should not be setting secondant_id.
I can only assume that your find_user method is getting invoked as a result of the User creation or the where operation you are executing in the debugger, resulting in the secondant_id field being set at that time. It won't be reflected in sec unless/until you do a reload, as the Ruby object created by where is distinct from the sec Ruby object.

Related

and sometimes I get failed spices because of Time.now, can't understand why

so i have a method in model
class << self
def last_week
start = Time.zone.now.beginning_of_week - 7.days
finish = start + 7.days
where('appointment_at >= ? AND appointment_at < ?', start, finish).order(appointment_at: :desc)
end
end
And I write spec for this method.
RSpec.describe Appointment, type: :model, vcr: { record: :none } do
let!(:time) { Time.now }
let(:appointment_at) { time }
context '.last_week' do
let!(:scoped_appointment) { create(:appointment, appointment_at: time - 2.days) }
let!(:another_appointment) { create(:appointment, appointment_at: time - 16.days) }
it do
travel_to(time) do
expect(Appointment.last_week).to include(scoped_appointment)
expect(Appointment.last_week).not_to include(another_appointment)
end
end
end
end
And sometime i get failed this spec with error.
expected #<ActiveRecord::Relation []> to include #<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, ... "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>
Diff:
## -1,2 +1,2 ##
-[#<Appointment id: 18, lead_id: 27, body: nil, appointment_at: "2019-02-25 00:59:47", google_id: nil, created_at: "2019-02-27 00:59:47", updated_at: "2019-02-27 00:59:47", timezone: nil, subject: "Meeting with Lead", address: nil, notification: nil, status: "pending", user_id: 22, notify: nil, cc_emails: nil, appointment_minutes: nil, status_message: nil>]
+[]
I can't understand why?
And I have a suggestion that I should tightly set time
in spec_helper.rb
$now = DateTime.parse('2020-01-01 00:00:01 -0500')
will it be right? and why ?
Your test setup is brittle. It will break depending on the day of the week you run your spec.
The scope in your model returns appointments from the previous week, Monday through Sunday (you are calling beginning_of_week and adding 7 days to it)
So if your tests run on a Wednesday, like in the example you provided, the appointment’s appointment_at field will be set to Monday (since you are calculating it as Time.now - 2.days). That means your scope will not cover that appointment.
I suggest you use a specific time in your setup. Given your current setup, using let(:time) { DateTime.parse('2019-02-25 00:00:00') } should work

Testing Cronjobs in Rails

I have a cronjob, which moves users from one table to another according some deadline reached.
This cronjob works in rails console, but the test is red. If I test the function from this cronjob, the test is green. When I go inside the cronjob with binding.pry, it holds all necessary variables and does its work correctly.
What can be wrong?
Test:
describe 'try various methods' do
before(:each) do
Obparticipant::Participant.all.delete_all
#content = FactoryBot.create(:content, :with_department_ob, target_group: 'child', subject: 'Infos für Teilnehmer aus {ort}', message: '«{geschlecht} | Lieber | Liebe» {vorname}, du bist am {geburtsdatum} geboren.', notification_email: '{nachname}, {straße}, {plz}, {wohnland}, {bundesland}, {landesgruppe}')
germany = ::Physical::Base::Country.GERMAN
address = FactoryBot.create(:address, addressline_1: 'Sesamstraße', addressline_2: 'Kaufmannstraße', state: 'Bayern', city: 'München', zip: '80331', country_id: germany.id)
person = FactoryBot.create(:person, firstname: 'Pablo', lastname: 'Domingo', dateofbirth: Date.new(2001,2,3), gender: 'm', address_id: address.id)
#participant = FactoryBot.create(:participant, person_id: person.id)
#participant.open_todos.by_task(:account_data).each{ |t| t.complete! }
end
it 'should move recipients with a start_date of today back to content_recipients' do
person_two = FactoryBot.create(:person)
participant_two = FactoryBot.create(:participant, person_id: person_two.id, program_season_id: #participant.program_season_id)
participant_two.open_todos.by_task(:account_data).each{ |t| t.complete! }
filter = '{"program_season":"' + #participant.program_season_id.to_s + '"}'
#content.update_attributes(for_dynamic_groups: true, filter: filter, is_draft: false, delay_days: 5)
FactoryBot.create(:delayed_content_recipient, content_id: #content.id, recipient_id: participant_two.id, start_date: Date.today)
expect(#content.content_recipients.size).to eq(0)
Cronjobs.check_recipients # or #content.insert_open_recipients
expect(#content.delayed_content_recipients.size).to eq(1)
expect(#content.content_recipients.map(&:recipient_id).last).to eq(participant_two.id) # this expectation fails, when a cronjob is tested, and passes, when a function is tested
end`
Cronjob:
def self.check_recipients
contents = ::Content.published.current.by_for_dynamic_groups(true)
contents.each do |content|
content.insert_open_recipients
end
end
Function
def insert_open_recipients
search = ::SimpleParticipantSearch.new(JSON.parse(self.filter))
new_recipients = search.result.without_content(self.id)
new_recipients.each do |nr|
if self.delay_days.present?
unless self.delayed_content_recipients.map(&:recipient_id).include?(nr.id)
self.delayed_content_recipients.create(content_id: self.id, recipient_id: nr.id, start_date: Date.today + self.delay_days.days)
end
else
self.participant_recipients << nr unless errors_with_participant?(nr)
end
end
if self.delayed_content_recipients.any?
self.delayed_content_recipients.each do |recipient|
if new_recipients.map(&:id).include?(recipient.recipient_id)
if recipient.start_date == Date.today
self.delayed_content_recipients.delete(recipient)
self.participant_recipients << Obparticipant::Participant.find_by(id: recipient.recipient_id) unless errors_with_participant?(Obparticipant::Participant.find_by(id: recipient.recipient_id))
end
else
self.delayed_content_recipients.delete(recipient)
end
end
end
end
The solution I found is to test separately whether a Cronjob is run, and whether the function it calls works.
I wrote a stub for this Cronjob in the cronjobs controller rspec
it 'should call the correct method on the Cronjobs.check_recipients object' do
Cronjobs.stub(:check_recipients)
post :create, job: 'CheckRecipients'
expect(Cronjobs).to have_received(:check_recipients)
expect(response).to have_http_status(200)
end
and tested the function in the test i provided above.
it 'should move recipients with a start_date of today back to content_recipients' do
person_two = FactoryBot.create(:person)
participant_two = FactoryBot.create(:participant, person_id: person_two.id, program_season_id: #participant.program_season_id)
participant_two.open_todos.by_task(:account_data).each{ |t| t.complete! }
filter = '{"program_season":"' + #participant.program_season_id.to_s + '"}'
#content.update_attributes(for_dynamic_groups: true, filter: filter, is_draft: false, delay_days: 5)
FactoryBot.create(:delayed_content_recipient, content_id: #content.id, recipient_id: participant_two.id, start_date: Date.today)
expect(#content.content_recipients.size).to eq(0)
#content.insert_open_recipients
expect(#content.delayed_content_recipients.size).to eq(1)
expect(#content.content_recipients.map(&:recipient_id).last).to eq(participant_two.id)
end

Rails Copying attributes from User object to new object

In the User model I have an archive! method that is called when a User is destroyed. This action creates a new ArchivedUser in separate table.
The ArchivedUser is successfully created, but the way I am manually setting each value is pretty dirty; if a new column is added to the User table it must be added here as well.
I tried to select and slice the attributes, but got undefined local variable or methoduser'`
ArchivedUser.create(user.attributes.select{ |key, _| ArchivedUser.attribute_names.include? key })
ArchivedUser.create(user.attributes.slice(ArchivedUser.attribute_names))
How can I iterate through each attribute in the User table when creating an ArchivedUser with self?
def archive!
if ArchivedUser.create(
user_id: self.id,
company_id: self.company_id,
first_name: self.first_name,
last_name: self.last_name,
email: self.email,
encrypted_password: self.encrypted_password,
password_salt: self.password_salt,
session_token: self.session_token,
perishable_token: self.perishable_token,
role: self.role,
score: self.score,
created_at: self.created_at,
updated_at: self.updated_at,
api_key: self.api_key,
device_id: self.device_id,
time_zone: self.time_zone,
device_type: self.device_type,
verified_at: self.verified_at,
verification_key: self.verification_key,
uninstalled: self.uninstalled,
device_details: self.device_details,
is_archived: self.is_archived,
registered_at: self.registered_at,
logged_in_at: self.logged_in_at,
state: self.state,
creation_state: self.creation_state,
language_id: self.language_id,
offer_count: self.offer_count,
expired_device_id: self.expired_device_id,
unique_id: self.unique_id,
best_language_code: self.best_language_code,
offer_id: self.offer_id,
vetted_state: self.vetted_state,
photo_path: self.photo_path
)
self.is_archived = true
self.email = "#{self.email}.archived#{Time.now.to_i}"
self.encrypted_password = nil
self.password_salt = nil
self.session_token = nil
self.perishable_token = nil
self.device_id = nil
self.verification_key = nil
self.save!
self.update_column(:api_key, nil)
UserGroup.delete_all(:user_id => self.id)
else
# handle the ArchivedUser not being created properly
end
end
Thanks for viewing :)
Update:
We were able to figure out the reasons why ArchivedUser.create(self.attributes.slice!(ArchivedUser.attribute_names) wasn't working. The first reason is the create method requires "bang" to write the object. The second reason was that ArchivedUser has a user_id field, that User doesn't receive until after create. We have to set the user_id: manually with merge(user_id: self.id)
The final output looks like
ArchivedUser.create!(self.attributes.slice!(ArchivedUser.attribute_names).merge(user_id: self.id))
You were on the right track with the first implementation. You just have to use self instead of user.
ArchivedUser.create(self.attributes.slice(ArchivedUser.attribute_names))
If you just want to have a copy of the user that is being archived, I think an elegant way would be to do
archived_user = user_to_be_archived.dup
or you can take a look at the amoeba gem, this will do all the heavy lifting including associations if you want.
https://github.com/amoeba-rb/amoeba

Created objects in Rspec test can be inspected but showing as blank? and !present?

I have a really strange case I'm finding when I'm running RSpec tests.
After doing a my_model = MyModel.create, I can do a my_model.inspect and I will get all the create properties of that object, but when I do a my_model.blank? or my_model.present? it shows as true and false respectively
I can further demonstrate the strange behavior by doing a
find_test = MyModel.find(my_model.id)
And I get the same results - the object can be inspected, shows properties, but registers as blank and not present.
Really weird and I can't write a reliable test with this behavior. How can something so obviously "exist" but be both "blank" and not "present"?
Here is code showing the output in comments:
def self.find_or_create(id, current_user)
cart = Cart.where(referral_guid: nil, id: id).first
if !priority_cart?(cart) && !current_user.nil?
cart = Cart.where(referral_guid: current_user.referral_guid).order("id DESC").first
end
cart.update_cart_with_referral_guid(current_user) unless current_user.nil?
cart = Cart.create unless cart
##### test 1 #####
puts("cart.inspect: #{cart.inspect}") #<Cart id: 2, created_at: "2015-06-13 17:39:57", updated_at: "2015-06-13 17:39:57", price_total: nil, tax: nil, referral_guid: nil>
puts("cart.present?: #{cart.present?}") #false
puts("cart.blank?: #{cart.blank?}") #true
##### test 2 #####
find_test = Cart.find(cart.id)
puts("find_test.inspect: #{find_test.inspect}") #<Cart id: 2, created_at: "2015-06-13 17:39:57", updated_at: "2015-06-13 17:39:57", price_total: nil, tax: nil, referral_guid: nil>
puts("find_test.present?: #{find_test.present?}") #false
puts("find_test.blank?: #{find_test.blank?}") #true
cart
end
def self.find_or_create(id, current_user)
cart = Cart.where(referral_guid: nil, id: id).first
if !priority_cart?(cart) && !current_user.nil?
cart = Cart.where(referral_guid: current_user.referral_guid).order("id DESC").first
end
cart.update_cart_with_referral_guid(current_user) unless current_user.nil?
cart = Cart.create unless cart
##### test 1 #####
puts("cart.inspect: #{cart.inspect}") #<Cart id: 2, created_at: "2015-06-13 17:39:57", updated_at: "2015-06-13 17:39:57", price_total: nil, tax: nil, referral_guid: nil>
puts("cart.present?: #{cart.present?}") #false
puts("cart.blank?: #{cart.blank?}") #true
##### test 2 #####
find_test = Cart.find(cart.id)
puts("find_test.inspect: #{find_test.inspect}") #<Cart id: 2, created_at: "2015-06-13 17:39:57", updated_at: "2015-06-13 17:39:57", price_total: nil, tax: nil, referral_guid: nil>
puts("find_test.present?: #{find_test.present?}") #false
puts("find_test.blank?: #{find_test.blank?}") #true
cart
end
blank? is a Rails method defined as follows:
def blank?
respond_to?(:empty?) ? !!empty? : !self
end
and present? is simply the inverse.
In other words, it's very much dependent on whether/how the model responds to the empty? method.
You haven't provided the definition for the Cart class and perhaps you're making use of some framework, but assuming this is some form of "shopping cart", it would be natural for it to have defined an empty? method which returns true if an only if it "contains" items. Since you just created the cart objects and haven't placed any items in it, it's natural that it would be "empty".

Access attributes in ActiveRecord::Associations::CollectionProxy

I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
I am wondering how i can access the keys/values in the contained hash and run a validation on it
if i do this
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
ap(a)
I get this returned
#<ActiveRecord::Associations::CollectionProxy [
#<AnimalImage id: 520, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 521, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 522, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">,
#<AnimalImage id: 523, animal_id: 158, image: "yp2.jpg", created_at: "2014-10-15 13:45:11", updated_at: "2014-10-15 13:45:11">
]
So i thought of using .map
animal = FactoryGirl.create(:animal, images_count: 4)
a = animal.animal_images
map = a.each.map { |i| i.image }
if map.length >= 4
ap('MORE THAN 4 IMAGES')
end
"MORE THAN 4 IMAGES"
So that iterates through the CollectionProxy. However how can i get this formatted into a correct rspec test and perform the logic in a custom validation function.
I thought my test would look like this
it 'should display an error message when too many images are uploaded' do
animal = FactoryGirl.create(:animal, images_count: 4)
animal.max_num_of_images
expect(animal.errors[:base]).to include("Max of 3 images allowed")
end
and just to get the pass for now (add error message) with no logic i have
class AnimalImage < ActiveRecord::Base
belongs_to :animal
validate :max_num_of_images, :if => "image?"
def max_num_of_images
errors.add(:base, "Max of 3 images allowed")
end
end
but it seems as if the test doesnt get past the first line
ActiveRecord::RecordInvalid:
Validation failed: Max of 3 images allowed
the above is thrown in the console
This is my factory
FactoryGirl.define do
factory :animal, class: Animal do
ignore do
images_count 0
end
after(:create) do |animal, evaluator|
create_list(:animal_image, evaluator.images_count, animal: animal)
end
end
end
FactoryGirl.define do
factory :animal_image do
image { File.open("#{Rails.root}/spec/fixtures/yp2.jpg") }
end
end
i'm probably going about this in the most backwards way possible, does anyone have any suggestions please
thanks
I am trying to write a custom function that will throw an error if the amount of associated objects are >=4
You are overcomplicating things. If you just want to count the number of records in a collection then you can simply do animal.animal_images.size. So your model will look like this:
class Animal < ActiveRecord::Base
has_many :animal_images
validate :max_num_of_images
def max_num_of_images
errors.add(:base, "Max of 3 images allowed") if self.animal_images.size >= 4
end
end

Resources