I got this problem where my test fails because the validation presence: true didn't work. I wonder why it didn't trigger. So, here's the code that I have:
A simple update in controller
def update
person = Person.find_by( params[:id] )
if person.update( params_person )
render json: {}, status: :ok
else
render json: person.errors, status: :422
end
end
def params_person
params.require( :person ).permit( :firstname, :lastname, ... , hobbies: [] )
end
The hobbies: [] is an array of strings hobbies like ['playing dota', 'basketball', 'coding', ... ]
Person model
class Person < ActiveRecord::Base
...
validates :hobbies, presence: true
...
end
if I did a request which
{ ...
...
hobbies: nil/'',
...
...
}
the validation still passing without update hobbies as the data nil/''
Updated
describe '#update' do
let( :user ) { create( :user ) }
fcontext 'when params missing' do
before do
put :update, id: person.id,
person: {
....,
....,
hobbies: nil
}
end
it 'respond an error message' do
expect( response_body ).to include( "Hobbies can't be blank" )
end
it { expect( response ).to have_http_status( 422 ) }
end
When you put hobbies: [] in strong_params definition and then proceed to pass a scalar to hobbies (your nil there), it won't be permitted by strong params and, therefore, will not be touched in an update.
To verify this, inspect params_person in that test.
Related
I am working on a project that has the following validation criteria in the "project" model:
validates :project_name,
presence: true,
format: { with: /\A[a-zA-Z\s_-]+\z/,
message: 'allows letters, spaces, underscores, and hyphens' }
validates :jira_id,
uniqueness: true,
presence: true,
format: { with: /\A[A-Z]+-[0-9]+\z/,
message: 'allows capitalized letters followed by a hyphen and numbers' }
validates :capacity,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :openings,
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_format_of :last_status_change_dt,
with: /\d{4}-\d{2}-\d{2}/,
message: 'must be formatted as YYYY-MM-DD',
on: :save
validates_presence_of :last_status_change_dt
The projects_controller:
def create
ActiveRecord::Base.transaction do
parameters = project_params.dup
parameters[:language_id] = get_language_id(params[:language_name]) if parameters[:language_id].nil?
#project = Project.new(parameters)
if #project.save
render json: #project, status: :created, location: #project
else
render json: #project.errors, status: :not_acceptable
raise ActiveRecord::Rollback
end
end
rescue ActiveRecord::ActiveRecordError
render json: { text: 'Project was not created' }, status: :internal_server_error
end
In the spec file, FactoryBot build and create functions are used.
In the tests right now, the validation is dependent on the project model, but I want to test the save function. How can I mock the save function to fail?
You can use allow_any_instance_of, but that's a bit of a hammer. Instead, make a double and mock Project.new to return it.
context 'Project#save raises ActiveRecord::ActiveRecordError' do
let(:project) {
# Set up the double to fail on save.
instance_double("Project").tap { |project|
allow(project).to receive(:save).and_raise(ActiveRecord::ActiveRecordError)
}
}
before {
# Set up Project.new to return the double
allow(Project).to receive(:new).and_return(project)
}
end
Note that it's quite unusual for save to raise ActiveRecord::ActiveRecordError, if it does something very unusual has gone wrong. You generally don't rescue it in a controller.
If your goal is to examine the response object and see if the json errors are returned correctly, I typically do something like this:
context "when saving the project fails" do
let(:params} do
{
language_id: 123,
}
end
let(:subject) { post :create, params: params }
before do
allow_any_instance_of(Project).to receive(:valid?) do |project|
project.errors.add(:language_id, 'fake validation reason')
false
end
end
it 'should return an error response' do
subject
expect(response).to have_http_status(422)
errors = response.parsed_body['errors']
expect(errors).to be_present
expect(errors['language_id']).to include 'fake validation reason'
end
end
You can also re-write the above code with instance_double.
I've been using attr_accessible for a long time, and I'm struggling a bit adopting to strong params.
models
class Rule
end
class Account
belongs_to :applied_rule, class_name: 'Rule', foreign_key: 'rule_id', inverse_of: false, optional: true
accepts_nested_attributes_for :applied_rule, update_only: true, allow_destroy: true
end
I'm trying to update the relation, and not having much success. With attr_accessible you would expose the relation itself, then use something like #account.update(applied_rule: #rule) and it would just workâ˘.
controllers
class AccountsController
def update
if #account.update(account_params)
render json: AccountSerializer.new(#account)
else
render json: #account.errors, status: :unprocessable_entity
end
end
private
def account_params
params.require(:account).permit(:name, applied_rule_attributes: %i(id _destroy))
end
end
specs
RSpec.describe 'Accounts', type: :request do
let(:account) { create(:account) }
describe 'PUT /accounts/:id' do
before { put account_path(account, params.merge(format: :json)) }
let(:rule) { create(:rule) }
context 'with good params' do
let(:params) { { account: { applied_rule_attributes: { id: rule.id } } } }
it { expect(response).to have_http_status(:ok) }
it { expect(account.changed?).to be true }
it { expect(account.applied_rule).to eq rule }
end
context 'when deleting relation' do
let(:params) { { account: { applied_rule_attributes: { _destroy: true } } } }
it { expect(response).to have_http_status(:unprocessable_entity) }
it { expect(account.changed?).to be true }
it { expect(account.applied_rule).to be_nil }
end
end
end
I tried this originally without the nested attributes - it still doesn't work, but I feel like it's heading in the correct direction.
I'd like to change the relation on the entity. I want to set the applied rule on an account to something different, or maybe even remove the applied rule from the account entirely (without deleting the rule, just the association). Is there an idiomatic approach to this?
interesting code
> params[:account][:applied_rule] = Rule.friendly.find(params[:account][:rule_id])
> params
=> <ActionController::Parameters {"account"=><ActionController::Parameters {"rule_id"=>"065230e1cb530d408e5d", "applied_rule"=>#<Rule id: 1, account_id: 3, name: "Rule 1", global: false, created_at: "2018-10-12 00:55:49", updated_at: "2018-10-12 00:55:49", slug: "065230e1cb530d408e5d">} permitted: false>, "controller"=>"accounts", "action"=>"update", "id"=>"account-2", "format"=>"json"} permitted: false>
> params.require(:account).permit(:name, :applied_rule)
=> <ActionController::Parameters {} permitted: true>
I have a model event and another model event_rule
class Event < ApplicationRecord
has_many :event_rules
end
class EventRule < ApplicationRecord
belongs_to :event
end
I have written an api event#create for saving an event. Here's the body of the POST request:
{
"name": "asd",
"code": "ad",
"isActive": true,
"description": "asd",
"notes": "",
"goalAmount": 0,
"exportId": "",
"defaultCurrency": 1,
"eventStartDate": "2017-04-25T18:30:00.000Z",
"eventEndDate": "2017-04-27T18:30:00.000Z",
"eventRules": [
{
"extraInformation": "{}",
"lookupKeyValuePairId": 40
}
]
}
Here's params hash:
Parameters: {"name"=>"asd", "code"=>"ad", "is_active"=>true, "description"=>"asd", "notes"=>"", "goal_amount"=>0, "export_id"=>"", "default_currency"=>1, "event_start_date"=>"2017-04-25T18:30:00.000Z", "event_end_date"=>"2017-04-27T18:30:00.000Z", "event_rules"=>[{"extra_information"=>"{}", "lookup_key_value_pair_id"=>40}], "client_id"=>"0", "event"=>{"name"=>"asd", "code"=>"ad", "description"=>"asd", "is_active"=>true, "goal_amount"=>0, "export_id"=>"", "event_start_date"=>"2017-04-25T18:30:00.000Z", "event_end_date"=>"2017-04-27T18:30:00.000Z", "default_currency"=>1, "notes"=>""}}
I want the 'event_rules' to be included INSIDE the event. How can do this?
def create
# initialize Event object with `event_params`
event = Event.new(event_params)
# initialize EventRule object per each `event_rule_params`, and associate the EventRule as part of `event.event_rules`
event_rules_params.each do |event_rule_params|
event.event_rules << EventRule.new(event_rule_params)
end
if event.save
# SUCCESS
else
# FAILURE
end
end
private
def event_params
params.require(:event).permit(:name, :code, :is_active, :description, :notes, :goal_amount, :export_id, :default_currency, :event_start_date, :event_end_date, :notes)
end
def event_rules_params
params.require(:event).fetch(:event_rules, []).permit(:extra_information, :lookup_key_value_pair_id)
end
Alternative Rails-way Solution:
if you have control over the parameters that get sent, reformat your request into something like the following (take note of changing event_rules into event_rules_attributes -- Rails Standard) (More Info Here)
Parameters: {
"event"=>{
"name"=>"asd",
"code"=>"ad",
"description"=>"asd",
"is_active"=>true,
"goal_amount"=>0,
"export_id"=>"",
"event_start_date"=>"2017-04-25T18:30:00.000Z",
"event_end_date"=>"2017-04-27T18:30:00.000Z",
"default_currency"=>1,
"notes"=>"",
"event_rules_attributes"=>[
{
"extra_information"=>"{}",
"lookup_key_value_pair_id"=>40
}
]
}
}
# controllers/events_controller.rb
def create
event = Event.new(event_params)
if event.save
# SUCCESS
else
# FAILURE
end
end
private
def event_params
params.require(:event).permit(:name, :code, :is_active, :description, :notes, :goal_amount, :export_id, :default_currency, :event_start_date, :event_end_date, :notes, event_rules_attributes: [:extra_information, :lookup_key_value_pair_id])
end
# models/event.rb
accepts_nested_attributes_for :event_rules
I'm a beginner in ruby on rails and programming in general.
I have an assignment where I have to test my rspec model Vote, and as per instructions the test should pass.
When I run rspec spec/models/vote_spec.rb on the console, I receive the following error:
.F
Failures:
1) Vote after_save calls `Post#update_rank` after save
Failure/Error: post = associated_post
NameError:
undefined local variable or method `associated_post' for #<RSpec::ExampleGroups::Vote::AfterSave:0x007f9416c791e0>
# ./spec/models/vote_spec.rb:22:in `block (3 levels) in <top (required)>'
Finished in 0.28533 seconds (files took 2.55 seconds to load)
2 examples, 1 failure
Failed examples:
rspec ./spec/models/vote_spec.rb:21 # Vote after_save calls `Post#update_rank` after save
Here is my vote_spec code:
require 'rails_helper'
describe Vote do
describe "validations" do
describe "value validation" do
it "only allows -1 or 1 as values" do
up_vote = Vote.new(value: 1)
expect(up_vote.valid?).to eq(true)
down_vote = Vote.new(value: -1)
expect(down_vote.valid?).to eq(true)
invalid_vote = Vote.new(value: 2)
expect(invalid_vote.valid?).to eq(false)
end
end
end
describe 'after_save' do
it "calls `Post#update_rank` after save" do
post = associated_post
vote = Vote.new(value: 1, post: post)
expect(post).to receive(:update_rank)
vote.save
end
end
end
And here is my post_spec code:
require 'rails_helper'
describe Post do
describe "vote method" do
before do
user = User.create
topic = Topic.create
#post = associated_post
3.times { #post.votes.create(value: 1) }
2.times { #post.votes.create(value: -1) }
end
describe '#up_votes' do
it "counts the number of votes with value = 1" do
expect( #post.up_votes ).to eq(3)
end
end
describe '#down_votes' do
it "counts the number of votes with value = -1" do
expect( #post.down_votes ).to eq(2)
end
end
describe '#points' do
it "returns the sum of all down and up votes" do
expect( #post.points).to eq(1) # 3 - 2
end
end
end
describe '#create_vote' do
it "generates an up-vote when explicitly called" do
post = associated_post
expect(post.up_votes ).to eq(0)
post.create_vote
expect( post.up_votes).to eq(1)
end
end
end
def associated_post(options = {})
post_options = {
title: 'Post title',
body: 'Post bodies must be pretty long.',
topic: Topic.create(name: 'Topic name',description: 'the description of a topic must be long'),
user: authenticated_user
}.merge(options)
Post.create(post_options)
end
def authenticated_user(options = {})
user_options = { email: "email#{rand}#fake.com", password: 'password'}.merge(options)
user = User.new( user_options)
user.skip_confirmation!
user.save
user
end
I'm not sure if providing the Post and Vote models code is necessary.
Here is my Post model:
class Post < ActiveRecord::Base
has_many :votes, dependent: :destroy
has_many :comments, dependent: :destroy
belongs_to :user
belongs_to :topic
default_scope { order('rank DESC')}
validates :title, length: { minimum: 5 }, presence: true
validates :body, length: { minimum: 20 }, presence: true
validates :user, presence: true
validates :topic, presence: true
def up_votes
votes.where(value: 1).count
end
def down_votes
votes.where(value: -1).count
end
def points
votes.sum(:value)
end
def update_rank
age_in_days = ( created_at - Time.new(1970,1,1)) / (60 * 60 * 24)
new_rank = points + age_in_days
update_attribute(:rank, new_rank)
end
def create_vote
user.votes.create(value: 1, post: self)
# user.votes.create(value: 1, post: self)
# self.user.votes.create(value: 1, post: self)
# votes.create(value: 1, user: user)
# self.votes.create(value: 1, user: user)
# vote = Vote.create(value: 1, user: user, post: self)
# self.votes << vote
# save
end
end
and the Vote model:
class Vote < ActiveRecord::Base
belongs_to :post
belongs_to :user
validates :value, inclusion: { in: [-1, 1], message: "%{value} is not a valid vote."}
after_save :update_post
def update_post
post.update_rank
end
end
It seems like in the spec vote model, the method assosicated_post can't be retrieved from the post spec model?
You're absolutely right - because you defined the associated post method inside of post_spec.rb, it can't be called from inside vote_spec.rb.
You have a couple options: you can copy your associated post method and put it inside vote_spec.rb, or you can create a spec helper file where you define associated_post once and include it in both vote_spec.rb and post_spec.rb. Hope that helps!
In my rails app i defined a specific JSON-Format in my model:
def as_json(options={})
{
:id => self.id,
:name => self.name + ", " + self.forname
}
end
And in the controller i simply call:
format.json { render json: #patients}
So now im trying to define another JSON-Format for a different action but i dont know how?
How do i have to define another as_json or how can i pass variables to as_json? Thanks
A very ugly method but you can refactor it for better readability:
def as_json(options={})
if options.empty?
{ :id => self.id, :name => self.name + ", " + self.forname }
else
if options[:include_contact_name].present?
return { id: self.id, contact_name: self.contact.name }
end
end
end
Okay, I should give you a better piece of code, here it is:
def as_json(options = {})
if options.empty?
self.default_json
else
json = self.default_json
json.merge!({ contact: { name: contact.name } }) if options[:include_contact].present?
json.merge!({ admin: self.is_admin? }) if options[:display_if_admin].present?
json.merge!({ name: self.name, forname: self.forname }) if options[:split_name].present?
# etc etc etc.
return json
end
end
def default_json
{ :id => self.id, :name => "#{self.name}, #{self.forname}" }
end
Usage:
format.json { render json: #patients.as_json(include_contact: true) }
By defining hash structure by 'as_json' method, in respective model class i.e User model in (Example 1), it becomes the default hash stucture for active record(i.e., user) in json format. It cannot be overridden by any inline definitions as defined in Example: 2
Example 1:
class User < ActiveRecord::Base
.....
def as_json(options={})
super(only: [:id, :name, :email])
end
end
Example: 2
class UserController < ApplicationController
....
def create
user = User.new(params[:user])
user.save
render json: user.as_json( only: [:id, :name] )
end
end
Therefore, in this example when create action is executed 'user' is returned in ("only: [:id, :name, :email]") format not as ("only: [:id, :name]")
So, options = {} are passed to as_json method to specifiy different format for different methods.
Best Practice, is to define hash structure as constant and call it everwhere it is needed
For Example
Ex: models/user.rb
Here, constant is defined in model class
class User < ActiveRecord::Base
...
...
DEFAULT_USER_FORMAT = { only: [:id, :name, :email] }
CUSTOM_USER_FORMAT = { only: [:id, :name] }
end
Ex: controllers/user.rb
class UserController < ApplicationController
...
def create
...
render json: user.as_json(User::DEFAULT_USER_FORMAT)
end
def edit
...
render json: user.as_json(User::CUSTOM_USER_FORMAT)
end
end
Cool!