How do I raise two errors with graphql-ruby? - ruby-on-rails

I'm using the graphql-ruby gem and I have a mutation that updates a record. This is all working, but now I want to include the phony_rails gem to validate phone number.
The problem
As a dev with more FE experience, I'm not great with rails or ruby and I'm using this to learn. That said, I have two phone numbers that I want to validate -- home_phone_number and mobile_phone_number. My mutation argument looks like this:
# frozen_string_literal: true
module Mutations
module Person
class UpdatePerson < BaseMutation
visibility_role :introspect_admin
visibility_pundit_class PersonPolicy
argument :id, ID, required: true
argument :email, String, required: false
argument :home_phone_number, String, required: false
argument :mobile_phone_number, String, required: false
field :person, Types::Person::Person, null: true
def call(input = {})
current_user = context[:current_user]
authorize!(current_user)
person = Person.find(input[:id])
person.email = input[:email]
person.home_phone_number = input[:home_phone_number]
person.mobile_phone_number = input[:mobile_phone_number]
person.save!
{ error: nil, person: person }
end
# #param [User|nil] current_user
def authorize!(current_user)
raise Pundit::NotAuthorizedError, 'Not allowed to update person.' unless
PersonPolicy.new(current_user, nil).update?
end
end
end
end
Now I want to add validation to home_phone_number and mobile_phone_number. I have written my tests to look like this:
context 'invalid number home phone number' do
let(:variables) do
{
'input' => {
'id' => person.id,
'homePhoneNumber' => '123'
}
}
end
it 'should return an error if home phone is invalid' do
expect(subject).not_to contain_graphql_errors
expect(data_dig('error')).not_to be_nil
expect(data_dig('error', 'error')).to eq('standard_error')
expect(data_dig('error', 'description')).to eq('Home phone number must be valid')
end
end
context 'invalid number mobile phone number' do
let(:variables) do
{
'input' => {
'id' => person.id,
'mobilePhoneNumber' => '123'
}
}
end
it 'should return an error if mobile phone is invalid' do
expect(subject).not_to contain_graphql_errors
expect(data_dig('error')).not_to be_nil
expect(data_dig('error', 'error')).to eq('standard_error')
expect(data_dig('error', 'description')).to eq('Mobile phone number must be valid')
end
end
What I've tried
What I can get working is this, but not necessarily passing my tests:
def call(input = {})
current_user = context[:current_user]
authorize!(current_user)
validate_phone_numbers(input[:home_phone_number], input[:mobile_phone_number])
# ....
def validate_phone_numbers(home_phone_number, mobile_phone_number)
phone_numbers = [home_phone_number, mobile_phone_number]
phone_numbers.each do |contact|
raise StandardError, 'Phone Number must be valid' if !PhonyRails.plausible_number?(contact) #would this stop execution too?
end
end
As you can see, in doing this, I wouldn't be able to specify which is a home phone number vs mobile phone number.
I've also tried doing this one-by-one:
def validate_phone_numbers(home_phone_number, mobile_phone_number)
home_phone_number_valid = PhonyRails.plausible_number?(home_phone_number)
mobile_phone_number_valid = PhonyRails.plausible_number?(mobile_phone_number)
raise StandardError, 'Home phone number must be valid' if !home_phone_number_valid
raise StandardError, 'Mobile phone number must be valid' if !mobile_phone_number_valid
# Stop execution
return if !home_phone_number_valid || !mobile_phone_number_valid
end
The lines above also do not exactly work.
Some guidance would be immensely appreciated. Thank you!

Instead of returning { error: nil, person: person } in your resolver, try returning { error: errors_array, person: person } where errors_array contains any validation error messages from phony.
eg.
def call
...
errors_array = []
errors_array << 'Home Phone Number must be valid' if !PhonyRails.plausible_number?(input[:home_phone_number])
errors_array << 'Mobile Phone Number must be valid' if !PhonyRails.plausible_number?(input[:mobile_phone_number])
...
{ error: errors_array, person: person }
end
Also make sure cover the case where both the mobile and home phone numbers are invalid in your specs.
I hope this helps!

Related

Rails Validation numbericality fails on form object

Related/Fixed: Ruby on Rails: Validations on Form Object are not working
I have the below validation..
validates :age, numericality: { greater_than_or_equal_to: 0,
only_integer: true,
:allow_blank => true
}
It is not required, if entered needs to be a number. I have noticed that if someone types in a word instead of a number, the field value changes to 0 after submit and passes validation. I would prefer it to be blank or the entered value.
Update:
Still no solution, but here is more information.
rspec test
it "returns error when age is not a number" do
params[:age] = "string"
profile = Registration::Profile.new(user, params)
expect(profile.valid?).to eql false
expect(profile.errors[:age]).to include("is not a number")
end
Failing Rspec Test:
Registration::Profile Validations when not a number returns error when age is not a number
Failure/Error: expect(profile.errors[:age]).to include("is not a number")
expected [] to include "is not a number"
2.6.5 :011 > p=Registration::Profile.new(User.first,{age:"string"})
2.6.5 :013 > p.profile.attributes_before_type_cast["age"]
=> "string"
2.6.5 :014 > p.age
=> 0
2.6.5 :015 > p.errors[:age]
=> []
2.6.5 :016 > p.valid?
=> true
#Form Object Registration:Profile:
module Registration
class Profile
include ActiveModel::Model
validates :age, numericality: { greater_than_or_equal_to: 0,
only_integer: true,
:allow_blank => true
}
attr_reader :user
delegate :age , :age=, to: :profile
def validate!
raise ArgumentError, "user cant be nil" if #user.blank?
end
def persisted?
false
end
def user
#user ||= User.new
end
def teacher
#teacher ||= user.build_teacher
end
def profile
#profile ||= teacher.build_profile
end
def submit(params)
profile.attributes = params.slice(:age)
if valid?
profile.save!
true
else
false
end
end
def self.model_name
ActiveModel::Name.new(self, nil, "User")
end
def initialize(user=nil, attributes={})
validate!
#user = user
end
end
end
#Profile Model:
class Profile < ApplicationRecord
belongs_to :profileable, polymorphic: true
strip_commas_fields = %i[age]
strip_commas_fields.each do |field|
define_method("#{field}=".intern) do |value|
value = value.gsub(/[\,]/, "") if value.is_a?(String) # remove ,
self[field.intern] = value
end
end
end
The interesting thing is that if move the validation to the profile model and check p.profile.errors, I see the expected result, but not on my form object. I need to keep my validations on my form object.
If the underlying column in the DB is a numeric type, then Rails castes the value. I assume this is done in [ActiveRecord::Type::Integer#cast_value][1]
def cast_value(value)
value.to_i rescue nil
end
Assuming model is a ActiveRecord model where age is a integer column:
irb(main):008:0> model.age = "something"
=> "something"
irb(main):009:0> model.age
=> 0
irb(main):010:0>
This is because submitting a form will always submit key value pairs, where the keys values are strings.
No matter if your DB column is a number, boolean, date, ...
It has nothing to do with the validation itself.
You can access the value before the type cast like so:
irb(main):012:0> model.attributes_before_type_cast["age"]
=> "something"
If your requirements dictate another behaviour you could do something like this:
def age_as_string=(value)
#age_as_string = value
self.age = value
end
def age_as_string
#age_as_string
end
And then use age_as_string in your form (or whatever). You can also add validations for this attribute, e.g.:
validates :age_as_string, format: {with: /\d+/, message: "Only numbers"}
You could also add a custom type:
class StrictIntegerType < ActiveRecord::Type::Integer
def cast(value)
return super(value) if value.kind_of?(Numeric)
return super(value) if value && value.match?(/\d+/)
end
end
And use it in your ActiveRecord class through the "Attributes API":
attribute :age, :strict_integer
This will keep the age attribute nil if the value you are trying to assign is invalid.
ActiveRecord::Type.register(:strict_integer, StrictIntegerType)
[1]: https://github.com/rails/rails/blob/fbe2433be6e052a1acac63c7faf287c52ed3c5ba/activemodel/lib/active_model/type/integer.rb#L34
Why don't you add validations in frontend? You can use <input type="number" /> instead of <input type="text" />, which will only accept number from the user. The way I see you explaining the issue, this is a problem to be resolved in the frontend rather than backend.
You can read more about it here: Number Type Input
Please let me know if this doesn't work for you, I will be glad to help you.

Rails - Grape validations error

I have problem while updating the application from Rails 4 to Rails 5.0.2
When I try I have this error:
/projects/tx/app/api/api_v2/validations.rb:3:in `<module:Validations>': uninitialized constant Grape::Validations::Validator (NameError)
from /projects/tx/app/api/api_v2/validations.rb:2:in `<module:APIv2>'
from /projects/tx/app/api/api_v2/validations.rb:1:in `<top (required)>'
from /projects/tx/app/api/api_v2/deposits.rb:1:in `require_relative'
from /projects/tx/app/api/api_v2/deposits.rb:1:in `<top (required)>'
Try to find solution for this but not success at all. Maybe Grape change some naming.
The code inside validations.rb seems like this:
module APIv2
module Validations
class Range < ::Grape::Validations::Validator
def initialize(attrs, options, required, scope)
#range = options
#required = required
super
end
def validate_param!(attr_name, params)
if (params[attr_name] || #required) && !#range.cover?(params[attr_name])
raise Grape::Exceptions::Validation, param: #scope.full_name(attr_name), message: "must be in range: #{#range}"
end
end
end
end
end
File deposits.rb is like this:
require_relative 'validations'
module APIv2
class Deposits < Grape::API
helpers ::APIv2::NamedParams
before { authenticate! }
desc 'Get your deposits history.'
params do
use :auth
optional :currency, type: String, values: Currency.all.map(&:code), desc: "Currency value contains #{Currency.all.map(&:code).join(',')}"
optional :limit, type: Integer, range: 1..100, default: 3, desc: "Set result limit."
optional :state, type: String, values: Deposit::STATES.map(&:to_s)
end
get "/deposits" do
deposits = current_user.deposits.limit(params[:limit]).recent
deposits = deposits.with_currency(params[:currency]) if params[:currency]
deposits = deposits.with_aasm_state(params[:state]) if params[:state].present?
present deposits, with: APIv2::Entities::Deposit
end
desc 'Get details of specific deposit.'
params do
use :auth
requires :txid
end
get "/deposit" do
deposit = current_user.deposits.find_by(txid: params[:txid])
raise DepositByTxidNotFoundError, params[:txid] unless deposit
present deposit, with: APIv2::Entities::Deposit
end
desc 'Where to deposit. The address field could be empty when a new address is generating (e.g. for bitcoin), you should try again later in that case.'
params do
use :auth
requires :currency, type: String, values: Currency.all.map(&:code), desc: "The account to which you want to deposit. Available values: #{Currency.all.map(&:code).join(', ')}"
end
get "/deposit_address" do
current_user.ac(params[:currency]).payment_address.to_json
end
end
end
You can find the reason here, and you can find initial method signature here.
change validations.rb
module APIv2
module Validations
class Range < ::Grape::Validations::Validator
def initialize(attrs, options, required, scope, opts = {})
#range = options
#required = required
super
end
def validate_param!(attr_name, params)
if (params[attr_name] || #required) && !#range.cover?(params[attr_name])
raise Grape::Exceptions::Validation, param: #scope.full_name(attr_name), message: "must be in range: #{#range}"
end
end
end
end
end

Testing custom validation method with RSpec

I try to test validation method that check times overlap for activities.
There are three factories(two of them inherit from activity).
Factories:
activities.rb
FactoryGirl.define do
factory :activity do
name 'Fit Girls'
description { Faker::Lorem.sentence(3, true, 4) }
active true
day_of_week 'Thusday'
start_on '12:00'
end_on '13:00'
pool_zone 'B'
max_people { Faker::Number.number(2) }
association :person, factory: :trainer
factory :first do
name 'Swim Cycle'
description 'Activity with water bicycles.'
active true
day_of_week 'Thusday'
start_on '11:30'
end_on '12:30'
end
factory :second do
name 'Aqua Crossfit'
description 'Water crossfit for evereyone.'
active true
day_of_week 'Thusday'
start_on '12:40'
end_on '13:40'
pool_zone 'C'
max_people '30'
end
end
end
Activities overlaps when are on same day_of_week(activity.day_of_week == first.day_of_week), on same pool_zone(activity.pool_zone == first.pool_zone) and times overlaps.
Validation method:
def not_overlapping_activity
overlapping_activity = Activity.where(day_of_week: day_of_week)
.where(pool_zone: pool_zone)
activities = Activity.where(id: id)
if activities.blank?
overlapping_activity.each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
else
overlapping_activity.where('id != :id', id: id).each do |oa|
if (start_on...end_on).overlaps?(oa.start_on...oa.end_on)
errors.add(:base, "In this time and pool_zone is another activity.")
end
end
end
end
I wrote rspec test, but unfortunatelly invalid checks.
describe Activity, 'methods' do
subject { Activity }
describe '#not_overlapping_activity' do
let(:activity) { create(:activity) }
let(:first) { create(:first) }
it 'should have a valid factory' do
expect(create(:activity).errors).to be_empty
end
it 'should have a valid factory' do
expect(create(:first).errors).to be_empty
end
context 'when day_of_week, pool_zone are same and times overlap' do
it 'raises an error that times overlap' do
expect(activity.valid?).to be_truthy
expect(first.valid?).to be_falsey
expect(first.errors[:base].size).to eq 1
end
end
end
end
Return:
Failure/Error: expect(first.valid?).to be_falsey
expected: falsey value
got: true
I can't understand why it got true. First create(:activity) should be right, but next shouldn't be executed(overlapping).
I tried add expect(activity.valid?).to be truthy before expect(first.valid?..., but throws another error ActiveRecord::RecordInvalid. Could someone repair my test? I'm newbie with creation tests using RSpec.
UPDATE:
Solution for my problem is not create :first in test but build.
let(:first) { build(:first) }
This line on its own
let(:activity) { create(:activity) }
doesn't create an activity. It only creates an activity, when activity is actually called. Therefore you must call activity somewhere before running your test.
There are several ways to do so, for example a before block:
before { activity }
or you could use let! instead of just let.

Rails faker gem produces same product name

I'm trying to use rails Faker gem to produce unique product names to make sample Item models in the database. I've used Faker multiple times but for some reason I can't produce new product names. I've made the nameMaker function to avoid possible early repeats, but I get a record invalidation just after one insert. Does anyone know how I could fix this?
seed.rb:
98.times do |n|
name = Item.nameMaker
description = Faker::Lorem.sentence(1)
price = Item.priceMaker
item = Item.create!(
name: name,
description: description,
price: price)
end
item.rb:
class Item < ActiveRecord::Base
validates :name, presence: true, length: { maximum: 100 }
validates :description, presence: true,
length: { maximum: 1000 }
VALID_PRICE_REGEX = /\A\d+(?:\.\d{0,3})?\z/
validates :price, presence: true,
:format => { with: VALID_PRICE_REGEX },
:numericality => {:greater_than => 0}
validates_uniqueness_of :name
def Item.nameMaker
loop do
name = Item.newName
break if Item.find_by(name: name).nil?
end
return name
end
def Item.newName
Faker::Commerce.product_name
end
end
To get a unique name, enclose the faker in brackets. Eg
name { Faker::Commerce.product_name }
To achieve this, you could also make use of factory girl and when you want to create 98 different Items, you could have something like
factories/item.rb
FactoryGirl.define do
factory :item do
name { Faker::Commerce.product_name }
description { Faker::Lorem.sentence(1) }
price Faker::Commerce.price
end
end
in your spec file
let(:item) { create_list(:item, 98) }
You can add validates_uniqueness_of :name in your model. When you run seed method if there is already exists same name, it will throw error and skip to the next.
There is possibility that you will not have exactly 98 Items. You can increase number of times or edit Faker itself.
I figured it out after some experimentation, apparently the loop in some ways acts as like a function in terms of scoping. If you initialize a local variable in a loop, the function outside of the loop will not see it. In this case name always returning the string Item from the Item.nameMaker function. Thus the first attempt would always succeed and the second one would obtain the validation restriction.
def Item.nameMaker
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Name Item"
return name
end
I managed to fix this by initializing the local variable outside of the loop. By doing this the entire function now has visibility to the same local variable for some reason.
def Item.nameMaker
name = "" #initializing
loop do
name = Faker::Commerce.product_name # 'Random Product Name'
puts "Name: #{name}" # "Name: Random Product Name"
item = Item.find_by(name: name)
if item.nil?
puts "#{name} not found" # "Random Product Name not found"
break
else
end
end
puts "Returning Name #{name}" # "Returning Random Product Name"
return name
end

Rails model fails format validation, regex

I'm setting up a model for recordings with the following constraints
class Recording < ActiveRecord::Base
attr_accessible :agent_id, :confirmation, :filepath, :phone, :call_queue_id, :date
belongs_to :call_queue
PHONE_FORMAT = /^[0-9]+$|Unavailable/
validates_presence_of :call_queue_id, :agent_id, :phone, :filepath, :date
validates :phone, format: { with: PHONE_FORMAT }
end
and am trying to test it with the following spec
describe Recording do
let(:queue) { FactoryGirl.create(:call_queue) }
before { #recording = queue.recordings.build(FactoryGirl.attributes_for(:recording)) }
subject { #recording }
# Stuff omitted...
describe "phone" do
it "should be present" do
#recording.phone = ''
#recording.should_not be_valid
end
context "with a valid format" do
it "should only consist of digits" do
#recording.phone = 'ab4k5s'
#recording.should_not be_valid
end
it "should only match 'Unavailable'" do
#recording.phone = 'Unavailable'
#recording.should be_valid
end
end
end
end
The first two tests pass, but the third fails with the following:
Failure/Error: #recording.should be_valid
expected valid? to return true, got false
I tested my regex with rubular to make sure it was working, and then again using irb just to be sure. I'm really confused why this is failing.
EDIT:
I eventually got the specs to pass by changing my before statement in the rspec:
describe Recording do
let(:queue) { FactoryGirl.create(:call_queue) }
before(:each) { #recording = queue.recordings.create(FactoryGirl.attributes_for(:recording) }
# the rest is the same...
which ultimately makes sense to me, to a point. Was the reason everything was getting messed up (falses were returning true, and vice versa) because once an attribute made the record invalid, I couldn't change it again? It seems like that was the case, I just want to make sure.
try:
PHONE_FORMAT = /^([0-9]+|Unavailable)$/

Resources