Rails Minitest model method test - ruby-on-rails

I've got User model method which calculate a number of user membership. I want to it test by MiniTest. Here is what I have:
def member_for
time_diff = Time.current - created_at
result = ActiveSupport::Duration.build(time_diff.to_i).parts
return "Member for #{result[:years]}y, #{result[:months]}m" if result.key?(:years) && result.key?(:months)
if result.key?(:years) && result.key?(:days)
"Member for #{result[:years]}y, #{result[:days]}d"
elsif result.key?(:months)
"Member for #{result[:months]}m"
elsif result.key?(:days)
"Member for #{result[:days]}d"
end
end
I was trying to write some MiniTest:
test 'member for' do
user.created_at = 2.years.ago + 3.months + 2.days
user.member_for
end
private
def user
#user ||= users(:one)
end
But to be honest, I don't know how to compare if it returns the correct string.

You can decrease the cyclic complexity as well as removing several bugs which give the wrong duration (by omitting weeks and months) by simply iterating across the parts:
def member_for
time_diff = Time.current - created_at
parts = ActiveSupport::Duration.build(time_diff.to_i).parts
formatted = parts.except(:seconds).map do |unit, value|
"#{value} #{unit}"
end
"Member for #{formatted.to_sentance}"
end
You can use a simple lookup table or the I18n API if you want to have abbreviations instead of the full unit names. Array#to_sentance is from ActiveSupport.
You would then test this by:
class MemberTest < ActiveSupport::TestCase
test 'member_for without weeks' do
user.created_at = 2.years.ago + 3.months + 2.days
assert_equal(user.member_for, 'Member for 1 year, 3 months and 2 days')
end
test 'member_for with weeks' do
user.created_at = 2.years.ago + 2.weeks
assert_equal(user.member_for, 'Member for 1 year and 2 weeks')
end
private
def user
#user ||= users(:one)
end
end
However its questionable if this code really belongs in a model in the first place since its presentation and not buisness logic. I would say a helper or decorator/presenter is more suitible then cramming more logic into your models.

Related

Limit requests that users can send to 3 every 30 days. Ruby on Rails

I am learning to program and I am trying with Ruby on rails. I would like to restrict one functionality that allows users to send me introduction requests (3 introduction request per 30 days). I am not sure if I have to create a method first, for example:
def month
where("created_at <?", Date.today - 30.days))
end
I don't know if that method is correct and if I can integrate it within this piece of code:
def create
#introduction = CompanyIntroduction.create(intro_params)
if current_user #Admin can edit the user name and email
#introduction.user = current_user
#introduction.user_name = current_user.full_name
#introduction.user_email = current_user.email
end
if #introduction.save
flash[:success] = "Thank you. Your request is being reviewed by TechIreland."
else
flash[:error] = #introduction.errors.full_messages
end
redirect_back(fallback_location: user_companies_path)
end
You're close. You have the time comparison backwards though, and the method (assuming it's on your model) should be a class method or a scope.
scope :in_the_last_month, -> { where('created_at > ?', Date.today - 30.days) }
# or more elegantly
scope :in_the_last_month, -> { where(created_at: 30.days.ago..) }
Then in the controller you can check how many requests were made recently.
if CompanyIntroduction.in_the_last_month.count >= 3
# give some error
else
# continue
end
This code is simple enough that you don't actually need to make it into a method, just the code in the controller is probably fine.
if CompanyIntroduction.where(created_at: 30.days.ago..).count >= 3

rake task to expire customers points balance

i am trying to work out how to write a rake tasks that will run daily and find where the days remaining is 0 to update the column amount to zero.
I have the following methods defined in my model, though they don't exactly appear to be working as I am getting the following error in the view
undefined method `-#' for Mon, 27 Jun 2016:Date
def remaining_days
expired? ? 0 : (self.expire_at - Date.today).to_i
end
def expired?
(self.expire_at - Date.today).to_i <= 0
end
def expire_credits
if expired?
self.update(:expire_at => Date.today + 6.months, :amount => 0)
end
end
with the rake tasks i have never written of these and i thought i would be able to call a method of StoreCredit that would expire the points if certain conditions are met but i am not sure how this all works
task :expire_credits => :environment do
puts 'Expiring unused credits...'
StoreCredit.expire_credits
puts "done."
end
# model/store_credit.rb
# get all store_credits that are expired on given date, default to today
scope :expire_on, -> (date = Date.current) { where("expire_at <= ?", date.beginning_of_day) }
class << self
def expire_credits!(date = Date.current)
# find all the expired credits on particular date, and update all together
self.expire_on(date).update_all(amount: 0)
end
end
Since it's a rake task, I think it's more efficient to update all expired ones together
#rake file
result = StoreCredit.expire_credits!
puts "#{result} records updated"
Retrieve Record Count Update
class << self
def expire_credits!(date = Date.current)
# find all the expired credits on particular date, and update all together
records = self.expire_on(date)
records.update_all(amount: 0)
records.length
end
end
You call class method but define instance method. You will need to define class method:
def self.expire_credits

Testing for uniqueness of before_create field in Rails and Rspec

I have a private method that generates a unique open_id for each user. The open_id is also indexed on the database level. How do I write a model test for uniqueness in RSpec?
before_create: generate_open_id!
def generate_open_id!
begin
self.open_id = SecureRandom.base64(64)
end while self.class.exists?(open_id: self.open_id)
end
UPDATE: solution based on accepted answer below
def generate_open_id!
if !self.open_id
begin
self.open_id = SecureRandom.base64(64)
end while self.class.exists?(open_id: self.open_id)
end
end
#users = FactoryGirl.create_list(:user, 10)
#user_last = #users.last
subject { #user_last }
it "has a random open_id" do
base_64_regex = %r{^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$}
expect(#user_last.open_id).to match base_64_regex
end
it "has a unique open_id" do
expect {FactoryGirl.create(:user, open_id: #user_last.open_id)}.to raise_error(ActiveRecord::RecordNotUnique)
end
Refactoring your original code will make testing what you're trying to do much easier. Change your generate_open_id! method to this
def generate_open_id!
(open_id = SecureRandom.base64(64)) unless open_id
end
And now you can test with the following
# spec/models/some_model_spec.rb
describe SomeModel do
subject(:some_model){ FactoryGirl.create(:some_model) }
describe 'open_id attribute' do
it 'is a random base64 string' do
base_64_regex = %r{^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$}
expect(some_model.open_id).to match base_64_regex
end
it 'is unique' do
expect {FactoryGirl.create(:some_model, open_id: some_model.open_id)}.to raise_error(ActiveRecord::RecordInvalid)
end
end
end
You can use SecureRandom.uuid that will generate to you unique strings.
More info here.
Also, you can add validates_uniqueness_of :your_field that will do it for you.

Why doesn't my Object update?

I have this test:
describe 'check_account_status' do
it 'should send the correct reminder email one week prior to account disablement' do
# Three weeks since initial email
reverification = create(:reverification)
initial_notification = reverification.twitter_reverification_sent_at.to_datetime
ActionMailer::Base.deliveries.clear
Timecop.freeze(initial_notification + 21) do
Reverification.check_account_status
ActionMailer::Base.deliveries.size.must_equal 1
ActionMailer::Base.deliveries.first.subject.must_equal I18n.t('.account_mailer.one_week_left.subject')
reverification.reminder_sent_at.class.must_equal ActiveSupport::TimeWithZone
reverification.notification_counter.must_equal 1
must_render_template 'reverification.html.haml'
end
end
This test produces this error:
check_account_status#test_0001_should send the correct reminder email one week prior to account disablement [/Users/drubio/vms/ohloh-ui/test/models/reverification_test.rb:67]:
Expected: ActiveSupport::TimeWithZone
Actual: NilClass
Here is my code:
class Reverification < ActiveRecord::Base
belongs_to :account
FIRST_NOTIFICATION_ERROR = []
class << self
def check_account_status
Reverification.where(twitter_reverified: false).each do |reverification|
calculate_status(reverification)
one_week_left(reverification)
end
end
private
def calculate_status(reverification)
#right_now = Time.now.utc.to_datetime
#initial_email_date = reverification.twitter_reverification_sent_at.to_datetime
#notification_counter = reverification.notification_counter
end
def one_week_left(reverification)
# Check to see if three weeks have passed since the initial email
# and check to see if its before the one day notification before
# marking an account as spam.
if (#right_now.to_i >= (#initial_email_date + 21).to_i) && (#right_now.to_i < (#initial_email_date + 29).to_i)
begin
AccountMailer.one_week_left(reverification.account).deliver_now
rescue
FIRST_NOTIFICATION_FAILURE << account.id
return
end
update_reverification_fields(reverification)
end
end
def update_reverification_fields(reverification)
reverification.notification_counter += 1
reverification.reminder_sent_at = Time.now.utc
reverification.save!
reverification.reload
end
end
Forgive the indentation, but what seems to be the problem, is that my reverification object doesn't update when it leaves the check_account_status method. I've placed puts statements through out the code and I can see without a doubt that the reverification object is indeed updating. However after it leaves the update_reverification_fields and returns to the test block, the fields are not updated. Why is that? Has anyone encountered this?
I believe you have a scope issue, the methods you call from check_account_status certainly don't return the updated object back to your method and Ruby only passes parameters by value.
Try something like this instead:
def check_account_status
Reverification.where(twitter_reverified: false).each do |reverification|
reverification = calculate_status(reverification)
reverification = one_week_left(reverification)
end
end
private
def calculate_status(reverification)
# ...
reverification
end
def one_week_left(reverification)
# ...
reverification = update_reverification_fields(reverification)
reverification
end
def update_reverification_fields(reverification)
# ...
reverification
end
The problem is that reverification object in your test and objects inside of check_account_status are different instances of the same model.
def update_reverification_fields(reverification)
reverification.notification_counter += 1
reverification.reminder_sent_at = Time.now.utc
reverification.save!
reverification.reload
end
This reload here, it's doing nothing. Let's walk through your test.
# check_account_status runs a DB query, finds some objects and does things to them
Reverification.check_account_status
# this expectation succeeds because you're accessing `deliveries` for the
# first time and you don't have it cached. So you get the actual value
ActionMailer::Base.deliveries.size.must_equal 1
# this object, on the other hand, was instantiated before
# `check_account_status` was called and, naturally, it doesn't see
# the database changes that completely bypassed it.
reverification.reminder_sent_at.class.must_equal ActiveSupport::TimeWithZone
So, before making expectations on reverification, reload it, so that it pulls latest data from the DB.
reverification.reload # here
reverification.reminder_sent_at.class.must_equal ActiveSupport::TimeWithZone

What is the best way to handle date in a Rails Form Objects

I'm using Form Object as described in 7 Patterns to Refactor Fat ActiveRecord Models #3 and currently I have an issue with storing date.
Here's what I've done:
class MyModelForm
# ...
def initialize(my_model: MyModel.new, params: {})
#my_model, #params = my_model, params
#my_model.date_field_on = Date.from_params #params, "date_field_on" if #params.present?
end
end
Where Date.from_params is implemented like:
class Date
def self.from_params(params, field_name)
begin
year = params["#{ field_name }(1i)"]
month = params["#{ field_name }(2i)"]
day = params["#{ field_name }(3i)"]
Date.civil year.to_i, month.to_i, day.to_i if year && month && day
rescue ArgumentError => e
# catch that because I don't want getting error when date cannot be parsed (invalid)
end
end
end
I cannot just use #my_model.assign_attributes #params.slice(*ACCEPTED_ATTRIBUTES) because my params["date_field_on(<n>i)"] will be skipped and date will not be stored.
Is there a better approach to handle date fields using Form Objects?
As #davidfurber mentioned in comments it works great with Virtus gem.
class MyModelForm
include Virtus
# ...
attribute :date_field_on, Date
def initialize(params: {})
#my_model.assign_attributes params
end
end

Resources