RSpec failing - date comparison - ruby-on-rails

So I have an RSpec test that fails, I'm sure that the test is the issue as from a functionality perspective it works fine.
In short, end_time should not be before_start time. Upon saving the model this works correctly, only the RSpec is failing, any pointers would be greatly appreciated
chain_plan_spec.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: chain_plans
#
# id :bigint not null, primary key
# end_time :datetime
# start_time :datetime
# created_at :datetime not null
# updated_at :datetime not null
# faction_id :bigint not null
#
# Indexes
#
# index_chain_plans_on_faction_id (faction_id)
#
# Foreign Keys
#
# fk_rails_... (faction_id => factions.id)
#
require 'rails_helper'
RSpec.describe ChainPlan, type: :model do
it { is_expected.to validate_presence_of :start_time }
it { is_expected.to validate_presence_of :end_time }
it { is_expected.to validate_presence_of :faction }
it 'does not have a start date/time before an end date/time' do
cp = described_class.new
cp.start_time = Date.now
cp.end_time = cp.start - 1.minute
cp.save!
expect(cp).not_to be_valid
end
# TODO: end is 0 minute
# TODO start is 0 minute
end
chain_plan.rb
# frozen_string_literal: true
# == Schema Information
#
# Table name: chain_plans
#
# id :bigint not null, primary key
# end_time :datetime
# start_time :datetime
# created_at :datetime not null
# updated_at :datetime not null
# faction_id :bigint not null
#
# Indexes
#
# index_chain_plans_on_faction_id (faction_id)
#
# Foreign Keys
#
# fk_rails_... (faction_id => factions.id)
#
class ChainPlan < ApplicationRecord
belongs_to :faction
has_many :chain_plan_slots
validates :start_time, presence: { on: :create, message: "can't be blank" }
validates :end_time, presence: { on: :create, message: "can't be blank" }
validates :faction, presence: { on: :create, message: "can't be blank" }
validate :end_date_after_start_date?
private
def end_date_after_start_date?
return if end_time.blank? || start_time.blank?
errors.add(:end_date, 'must be after the start date') if end_time < start_time
end
end

When testing validations don't use expect(object).not_to be_valid or expect(object).to be_valid. This is just a recipe for both false positivies and negatives as you're not actually testing a single behavior - rather you're testing every single validation at once along with your test setup.
RSpec.describe ChainPlan, type: :model do
it 'does not allow an end_time which is after the start_time' do
cp = described_class.new(
start_time: Time.current,
end_time: Time.current - 1.minute
)
cp.valid?
expect(cp.errors.messages_for(:end_time)).to include 'must be after the start date'
end
it "allows a valid start_time/end_time combo" do
cp = described_class.new(
start_time: Time.current,
end_time: Time.current.advance(10.minutes)
)
cp.valid?
expect(cp.errors).to_not have_key :end_date
end
end
Instead setup the object and then call valid? on it to trigger the validations. Write expectations on the errors object to test the actual behavior instead of carpet bombing. For example here you would have completely missed that you where adding the errors to the key :end_date instead of :end_time.
The validation itself also can be improved:
class ChainPlan < ApplicationRecord
belongs_to :faction
has_many :chain_plan_slots
validates :start_time, presence: { on: :create, message: "can't be blank" }
validates :end_time, presence: { on: :create, message: "can't be blank" }
validates :faction, presence: { on: :create, message: "can't be blank" }
validate :end_date_after_start_date?, if: ->{ end_time.present? && start_time.present? }
private
def end_date_after_start_date?
errors.add(:end_time, 'must be after the start date') if end_time < start_time
end
end

I think the issue is Date.now, which is not a proper command - at least from documentation I could not find the now method for the class Date. Use Time.now instead. This worked fine for me.

Related

Why is numericality validator not working with Active Model Attributes?

I'm using Rails 7 and Ruby 3.1, and Shoulda Matchers for tests, but not Active Record, for I do not need a database.
I want to validate numericality. However, validations do not work. It looks like input is transformed into integer, instead of being validated. I do not understand why that happens.
My model:
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: { in: 1..50 }, numericality: { only_integer: true }
# Some other code...
end
My test:
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, range|
it { is_expected.to validate_numericality_of(field).only_integer }
it { is_expected.to validate_inclusion_of(field).in_range(range) }
end
include_examples 'validates', 'rows', 1..50
end
# Some other tests...
end
Nonetheless, my test fails:
Grid validations is expected to validate that :rows looks like an integer
Failure/Error: it { is_expected.to validate_numericality_of(field).only_integer }
Expected Grid to validate that :rows looks like an integer, but this
could not be proved.
After setting :rows to ‹"0.1"› -- which was read back as ‹0› -- the
matcher expected the Grid to be invalid and to produce the validation
error "must be an integer" on :rows. The record was indeed invalid,
but it produced these validation errors instead:
* rows: ["is not included in the list"]
As indicated in the message above, :rows seems to be changing certain
values as they are set, and this could have something to do with why
this test is failing. If you've overridden the writer method for this
attribute, then you may need to change it to make this test pass, or
do something else entirely.
Update
Worse than before, because tests are working but code is not actually working.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, presence: true, numericality: { only_integer: true, in: 1..50 }
# Some other code...
end
# spec/models/grid_spec.rb
RSpec.describe Grid, type: :model do
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
.is_greater_than_or_equal_to(range.min)
.is_less_than_or_equal_to(range.max)
.with_message("must be in #{range}")
is_expected.to type == :integer ? validate.only_integer : validate
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
# Some other tests...
end
The underlying problem (or maybe not a problem) is that you're typecasting rows attribute to integer.
>> g = Grid.new(rows: "num"); g.validate
>> g.errors.as_json
=> {:rows=>["is not included in the list"]}
# NOTE: only inclusion errors shows up, making numericality test fail.
To make it more obvious, let's remove inclusion validation:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, numericality: { only_integer: true }
end
Still, this does not fix numericality test:
# NOTE: still valid
>> Grid.new(rows: "I'm number, trust me").valid?
=> true
# NOTE: because `rows` is typecasted to integer, it will
# return `0` which is numerical.
>> Grid.new(rows: "I'm number, trust me").rows
=> 0
>> Grid.new(rows: 0.1).rows
=> 0
# NOTE: keep in mind, this is the current behavior, which
# might be unexpected.
In the test validate_numericality_of, first of all, expects an invalid record with "0.1", but grid is still valid, which is why it fails.
Besides replacing the underlying validations, like you did, there are a few other options:
You could replace numericality test:
it { expect(Grid.new(rows: "number").valid?).to eq true }
it { expect(Grid.new(rows: "number").rows).to eq 0 }
# give it something not typecastable, like a class.
it { expect(Grid.new(rows: Grid).valid?).to eq false }
Or remove typecast:
attribute :rows
Update
Seems like you're trying to overdo it with validations and typecasting. From what I can see the only issue is just one test, everything else works fine. Anyway, I've came up with a few more workarounds:
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
def rows= arg
# NOTE: you might want to raise an error instead,
# because this validation will go away if you run
# validations again.
errors.add(:rows, "invalid") if (/\d+/ !~ arg.to_s)
super
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
validates :rows, inclusion: 1..50, numericality: { only_integer: true }
validate :validate_rows_before_type_cast
def validate_rows_before_type_cast
rows = #attributes.values_before_type_cast["rows"]
errors.add(:rows, :not_a_number) if rows.is_a?(String) && rows !~ /^\d+$/
end
end
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveRecord::AttributeMethods::BeforeTypeCast
attribute :rows, :integer
validates :rows, inclusion: 1..50
# NOTE: this does show "Rows before type cast is not a number"
# maybe you'd want to customize the error message.
validates :rows_before_type_cast, numericality: { only_integer: true }
end
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/BeforeTypeCast.html
My solution was dividing validations and typecasts into models.
# app/models/grid.rb
class Grid
include ActiveModel::Model
include ActiveModel::Attributes
attribute :rows, :integer
end
# app/models/grid_data.rb
class GridData
include ActiveModel::Model
include ActiveModel::Attributes
Grid.attribute_names.each { |name| attribute name }
validates(*attribute_names, presence: true)
validates :rows, numericality: { only_integer: true, in: 1..50 }
end
Specs
# spec/models
RSpec.describe Grid, type: :model do
let(:grid) { described_class.new(**attributes) }
describe 'type cast' do
let(:attributes) { default(rows: '2') }
it 'parses string valid arguments to integer or float' do
expect(grid.rows).to eq 2
end
end
end
RSpec.describe GridData, type: :model do
it 'has same attributes as Grid model' do
expect(described_class.attribute_names).to eq Grid.attribute_names
end
describe 'validations' do
shared_examples 'validates' do |field, type, range|
it { is_expected.to validate_presence_of(field) }
it do
validate = validate_numericality_of(field)
validate = validate.only_integer if type == :integer
expect(subject).to validate
end
it do
expect(subject).to validate_inclusion_of(field)
.in_range(range)
.with_message("must be in #{range}")
end
end
include_examples 'validates', 'rows', :integer, 1..50
end
end
Controller
# app/controller/grids_controller.rb
class GridsController < ApplicationController
def create
#grid_data = GridData.new(**grid_params)
if #grid_data.valid?
play
else
render :new, status: :unprocessable_entity
end
end
private
def grid_params
params.require(:grid_data).permit(*Grid.attribute_names)
end
def play
render :play, status: :created
Grid.new(**#grid_data.attributes).play
end
end

undefined local variable or method for delayed_job

I'm trying to modify my existing callback on a App model to be executed via delayed job. I am getting a error undefined local variable or method app_name for main:Object, when deleted an app.
app/models/app.rb
# == Schema Information
#
# Table name: apps
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime
# updated_at :datetime
# app_type :string(255)
# package_name :string(255)
# icon :string(255)
#
# app/models/app.rb
class App < ActiveRecord::Base
has_many :versions, dependent: :destroy
app/models/version.rb
class Version < ActiveRecord::Base
DEFAULT_ICON_URL = 'placeholder_med#2x.png'
belongs_to :app
delegate :name, :id, :users, :app_type, to: :app, prefix: true
after_create :notify_subscribers
before_destroy :remove_stored_files
scope :since, ->(time) { where('created_at > ?', time) }
def updated_or_created_at
updated_at || created_at
end
def display_icon
if icon_url.blank? || icon_url.match(/default.png/)
DEFAULT_ICON_URL
else
icon_url
end
end
def main?
version_type == 'main'
end
def release_notes?
!release_notes_url.blank?
end
private
def notify_subscribers
AppMailer.notify_new_build(id)
end
def remove_stored_files
Delayed::Job.enqueue(DeleteAppFilesJob.new(app_name, version_number, build_number), priority: 1, run_at: 5.minute.from_now)
end
end
app/jobs/delete_app_files_job.rb
class DeleteAppFilesJob < Struct.new(app_name, version_number, build_number)
def perform
remove_stored_files(app_name, version_number, build_number)
end
protected
def remove_stored_files(app_name, version_number, build_number)
S3_BUCKET.objects.select { |obj| obj.key.match(%r{(ios|android)/#{app_name}/#{version_number}/#{build_number}}) }.each do |obj|
puts "Deleting #{obj.key}"
obj.delete
end
end
end
To create an anonymous Struct (which you generally want when subclassing like that), you pass Symbol arguments to Struct.new:
class DeleteAppFilesJob < Struct.new(:app_name, :version_number, :build_number)
You're trying to pass variables that don't exist, hence the "undefined local variable or method" error.

Same Issue in Two Tests: Something Wrong With Events

I believe something is wrong with the creation of events in my testing environment.
When I navigate in the browser everything is fine.
The two errors I get are:
1) Error:
test_should_post_save_period(PeriodRegistrationsControllerTest):
NoMethodError: undefined method `event' for nil:NilClass
2) Error:
test_should_get_index(PeriodsControllerTest):
ActionView::Template::Error: undefined method `name' for nil:NilClass
Error 1 test:
def setup
#period_registration= FactoryGirl.create(:period_registration)
end
test "should post save_period" do
sign_in(FactoryGirl.create(:user))
assert_difference('PeriodRegistration.count') do
post :save_period, period_registration: FactoryGirl.attributes_for(:period_registration)
end
assert_not_nil assigns(:period_registration)
# assert_response :success
end
Error 2 test:
test "should get index" do
sign_in(FactoryGirl.create(:user, admin: true))
get :index
assert_not_nil assigns(:periods)
assert_response :success
end
Error number one corresponds with this action in the controller:
def save_period
#period_registration = PeriodRegistration.new(params[:registration])
#period_registration.save
flash[:success] = "Successfully Registered for Session."
redirect_to event_url(#period_registration.period.event) #problem line
end
The second error corresponds with this line in my view:
<h6><%= period.event.name %> in <%= period.event.city %>, <%= period.event.state%></h6>
Here is my event factory:
factory :event do
name 'First Event'
street '123 street'
city 'Chicago'
state 'Iowa'
date Date.today
end
factory :period do
name 'First Period'
description 'This is a description'
start_time Time.now + 10.days
end_time Time.now + 10.days + 2.hours
event
product
end
factory :period_registration do
user
period
end
And my event model looks like this:
# == Schema Information
#
# Table name: events
#
# id :integer not null, primary key
# name :string(255)
# date :date
# street :string(255)
# city :string(255)
# state :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
class Event < ActiveRecord::Base
attr_accessible :city, :date, :name, :state, :street
has_many :periods
validates :name, presence: true
validates :street, presence: true
validates :city, presence: true
validates :state, presence: true
end
and here is my period model:
# == Schema Information
#
# Table name: periods
#
# id :integer not null, primary key
# name :string(255)
# event_id :integer
# created_at :datetime not null
# updated_at :datetime not null
# start_time :time
# end_time :time
# description :text
# product_id :integer
#
class Period < ActiveRecord::Base
attr_accessible :event_id, :name, :time, :start_time, :end_time, :description, :product_id
belongs_to :event
belongs_to :product
has_many :period_registrations
validates_time :end_time
validates_time :start_time
validates_presence_of :name
validates_presence_of :start_time
validates_presence_of :end_time
validates_presence_of :description
end
Any ideas on what could be causing this?
I think it's because FactoryGirl.attributes_for(:period_registration) returns {} (empty hash). You can check it in rails console. And also you have typo in code: in test you send period_registration: FactoryGirl.attributes_for(:period_registration), but in controller you expects params[:registration]. This leads to the empty PeriodRegistration model is created in db. This model does not contain event_id and when you request event from model, it returns nil.
Why you do not use mock for these kind of tests?
I think it's because you are missing your Factory for the User model

Don't get `should validate_inclusion_of` to work in Rails3.2, RSpec2

Model:
class Contact < ActiveRecord::Base
validates :gender, :inclusion => { :in => ['male', 'female'] }
end
Migration:
class CreateContacts < ActiveRecord::Migration
def change
create_table "contacts", :force => true do |t|
t.string "gender", :limit => 6, :default => 'male'
end
end
end
RSpec test:
describe Contact do
it { should validate_inclusion_of(:gender).in(['male', 'female']) }
end
Result:
Expected Contact to be valid when gender is set to ["male", "female"]
Anybody has an idea why this spec doesn't pass? Or can anybody reconstruct and (in)validate it? Thank you.
I misunderstood how the .in(..) should be used. I thought I could pass an array of values, but it seems it does only accept a single value:
describe Contact do
['male', 'female'].each do |gender|
it { should validate_inclusion_of(:gender).in(gender) }
end
end
I don't really know what's the difference to using allow_value though:
['male', 'female'].each do |gender|
it { should allow_value(gender).for(:gender) }
end
And I guess it's always a good idea to check for some not allowed values, too:
[:no, :valid, :gender].each do |gender|
it { should_not validate_inclusion_of(:gender).in(gender) }
end
I usually prefer to test these things directly. Example:
%w!male female!.each do |gender|
it "should validate inclusion of #{gender}" do
model = Model.new(:gender => gender)
model.save
model.errors[:gender].should be_blank
end
end
%w!foo bar!.each do |gender|
it "should validate inclusion of #{gender}" do
model = Model.new(:gender => gender)
model.save
model.errors[:gender].should_not be_blank
end
end
You need to use in_array
From the docs:
# The `validate_inclusion_of` matcher tests usage of the
# `validates_inclusion_of` validation, asserting that an attribute can
# take a whitelist of values and cannot take values outside of this list.
#
# If your whitelist is an array of values, use `in_array`:
#
# class Issue
# include ActiveModel::Model
# attr_accessor :state
#
# validates_inclusion_of :state, in: %w(open resolved unresolved)
# end
#
# # RSpec
# describe Issue do
# it do
# should validate_inclusion_of(:state).
# in_array(%w(open resolved unresolved))
# end
# end
#
# # Test::Unit
# class IssueTest < ActiveSupport::TestCase
# should validate_inclusion_of(:state).
# in_array(%w(open resolved unresolved))
# end
#
# If your whitelist is a range of values, use `in_range`:
#
# class Issue
# include ActiveModel::Model
# attr_accessor :priority
#
# validates_inclusion_of :priority, in: 1..5
# end
#
# # RSpec
# describe Issue do
# it { should validate_inclusion_of(:state).in_range(1..5) }
# end
#
# # Test::Unit
# class IssueTest < ActiveSupport::TestCase
# should validate_inclusion_of(:state).in_range(1..5)
# end
#

newbie: tips on how to cleanup , improve, arrange, shorten model?

I find my models getting more and more clumped and messy. below is my actual user model, any suggestions on cleaning things up and arranging things for better readability?
Im getting frustrated by the unreadability of this all, so any general thoughts on this code are welcome. I really like code that does the same to be positioned together but like I have now is just one big bulk unreadable mess.
# == Schema Information
#
# Table name: users
#
# id :integer(4) not null, primary key
# email :string(255) default(""), not null
# encrypted_password :string(255) default(""), not null
# reset_password_token :string(255)
# reset_password_sent_at :datetime
# remember_created_at :datetime
# sign_in_count :integer(4) default(0)
# current_sign_in_at :datetime
# last_sign_in_at :datetime
# current_sign_in_ip :string(255)
# last_sign_in_ip :string(255)
# password_salt :string(255)
# confirmation_token :string(255)
# confirmed_at :datetime
# confirmation_sent_at :datetime
# unconfirmed_email :string(255)
# failed_attempts :integer(4) default(0)
# unlock_token :string(255)
# locked_at :datetime
# authentication_token :string(255)
# username :string(255)
# is_blocked :boolean(1)
# is_deleted :boolean(1)
# role :string(255)
# slug :string(255)
# created_at :datetime not null
# updated_at :datetime not null
# last_seen :datetime
# credits :integer(4)
#
class User < ActiveRecord::Base
devise :database_authenticatable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable,
:token_authenticatable,
:encryptable,
:confirmable,
:lockable,
:timeoutable,
:lastseenable
#:omniauthable
attr_accessible :username,
:login,
:email,
:password,
:password_confirmation,
:remember_me,
:profile_attributes,
:is_moderated,
:is_blocked,
:is_deleted,
:credits,
:role,
:confirmed_at,
:last_seen,
:invite_code
attr_accessor :login
#attr_accessor :invite_code
has_one :profile
has_one :account
accepts_nested_attributes_for :profile
accepts_nested_attributes_for :account
extend FriendlyId
friendly_id :username, use: :slugged
before_create :default_values
# AFTER CREATE -------------------------------------------------------------------------------------------------------
after_create :add_account
def add_account
self.create_account
end
def default_values
self.credits = -1
self.invite_code = invite_code
#self.reset_authentication_token!
beta = Beta.where(:code => invite_code).first
beta.used = 1
beta.save
end
# ROLES --------------------------------------------------------------------------------------------------------------
before_create :setup_default_role_for_new_users
ROLES = %w[admin default vip]
# VALIDATIONS --------------------------------------------------------------------------------------------------------
before_validation { |u| u.username.downcase! }
before_validation { |u| u.email.downcase! }
validates_uniqueness_of :username,
:email,
:case_sensitive => false
validates_presence_of :email,
:username,
:invite_code
validates :username,
:exclusion => {:in => ["admin", "root", "administrator", "superuser", "myhost", "support", "contact", "chat", "boo"],
:message => "is reserved"}
validate :check_email, :on => :create
validate :check_invite_code, :on => :create
def check_invite_code
errors.add(:invite_code, "Invalid code") unless Beta.where(:code => invite_code, :used => 0).first
end
# Devise
def active_for_authentication?
super && !is_deleted
end
# Devise
def confirm!
#welcome_message
#super
end
# Devise
def soft_delete
update_attribute(:deleted_at, Time.current)
end
def is_moderated?
return self.is_moderated
end
def is_online?
if self.last_seen < 10.minutes.ago
return true
else
return false
end
end
private
def setup_default_role_for_new_users
if self.role.blank?
self.role = "default"
end
end
def welcome_message
#::Devise.mailer.welcome_instructions(self).deliver
::Devise.mailer.welcome_instructions(self).deliver
end
def check_email
host = email.split("#").last
username = email.split("#").first
reserved_word_filters = %w(admin hostmaster root support )
if /.*(#{reserved_word_filters.join("|")}).*\#/.match(email)
errors.add(:email, "Invalid username in email")
end
if email.include? 'myhost'
errors.add(:email, "Invalid email")
end
end
# DEVISE: --------------------------------------------------------------------------------------------------
protected
def self.find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", {:value => login.strip.downcase}]).first
end
end
The only thing I would do to clean up models is to modularize them a bit. DHH himself posted a great example gist showing how to clean up a model that had gotten too large. I don't think yours is particularly too big, but if you wanted to move all the devise stuff into its own module, it certainly would make your model a bit more tidy.
You could also get in the habit of treating booleans as first class expressions. For example,
def is_online?
if self.last_seen < 10.minutes.ago
return true
else
return false
end
end
is more clearly written as:
def is_online?
last_seen < 10.minutes.ago
end
1.
You have return statements all over your code. Unless you are returning something prematurely in a method, the return of the last statement in a method is what Ruby returns automatically. Like so:
def square(x)
val = x**2
return val
end
can be shortened to:
def square(x)
x**2
end
It's a contrived example, but there it is.
2.
Many of the selfs are redundant. In an model instance's scope, when setting an attribute to a value or calling a method, you do not need to prepend self, since that method/variable is already being called from that same scope.

Resources