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>
Related
I need to manage files attached to a model through nested attributes. The main problem is that the request has problems parsing the params hash and it joins some of those params together.
The model is the following:
class Logging::Note < ApplicationRecord
belongs_to :user
has_many :uploads, class_name: 'Logging::NoteUpload',
foreign_key: 'logging_note_id', inverse_of: :note, dependent: :destroy
accepts_nested_attributes_for :uploads, allow_destroy: true
end
this is the controller:
class API::V1::Logging::NotesController < API::V1::APIController
def update
authorize note
note.update(note_update_params) ? render_note : render_note_errors
end
private
def note_update_params
params.require(:note).permit(:text, :date, :title,
uploads_attributes: [:id, :_destroy, file: []])
end
end
and I'm using rspec to test it:
context 'when the user is logged in' do
subject(:put_request) do
put api_note_path(note.id), params: params, headers: auth_headers_with_group
end
let(:note) { create(:logging_note, user: author) }
let!(:note_upload) { create(:logging_note_upload, note: note) }
let!(:note_upload2) { create(:logging_note_upload, note: note) }
let(:params) do
{
note: {
text: new_text,
date: new_date,
title: new_title,
uploads_attributes: uploads_attributes
}
}
end
let(:uploads_attributes) do
[
{
id: note_upload.id,
_destroy: true
},
{
id: note_upload2.id,
_destroy: true
},
{
file: [test_picture('sample_graph.png')]
}
]
end
specify do
put_request
expect(response).to have_http_status(:success)
end
it 'updates the note record' do
expect {
put_request
}.to change(Logging::NoteUpload, :count).by(1)
expect(note.reload.text).to eq(new_text)
expect(note.title).to eq(new_title)
expect(note.date).to eq(new_date)
expect(note.uploads).not_to include(note_upload, note_upload2)
expect(note.reload.uploads.count).to eq(1)
end
end
when I'm setting this parameters and while debugging in the update method of the controller, note_update_params[:uploads_attributes].count should return 3, but it's returning 2 instead.
note_update_params[:uploads_attributes]:
[
<ActionController::Parameters {
"id"=>"e1a437ff-42b4-4cb2-af6c-0edf0a824265",
"_destroy"=>"true"
} permitted: true>,
<ActionController::Parameters {
"id"=>"a993722e-23c5-4e75-a755-aff4553201e3",
"_destroy"=>"true",
"file"=>[#<ActionDispatch::Http::UploadedFile:0x00007ff8a8099e00 #tempfile=#
<Tempfile:/var/folders/14/1rt4bsg94cj74_gc631xjnmm0000gn/T/RackMultipart20200214-
18920-1lmntho.png>, #original_filename="sample_graph.png", #content_type="image/png",
#headers="Content-Disposition: form-data; name=\"note[uploads_attributes][][file][]\";
filename=\"sample_graph.png\"\r\nContent-Type: image/png\r\nContent-Length:
51604\r\n">]
} permitted: true>
]
it is receiving this parameters keys:
["id", "_destroy"]
["id", "_destroy", "file"]
when it should be receiving the following:
["id", "_destroy"]
["id", "_destroy"]
["file"]
as this thread describes https://github.com/rails/rails/issues/23997 adding as: :json to the request should solve the parsing problem but json doesn't handle files so the controller receives an empty param instead.
["id", "_destroy"]
["id", "_destroy"]
[]
I need to test if my implementation of optimistic locking is correct or not. But I don't know how to test my functionalities. Here is the update action I wrote:
def update
begin
#update_operator = Operator.find(params[:id])
authorize! :update, #update_operator
if #update_operator.update_attributes(operator_params)
render json: #update_operator, except: :badge
else
render json: #update_operator.errors, status: :unprocessable_entity
end
rescue ActiveRecord::StaleObjectError
#update_operator.reload
retry
end
end
And here is the migration I added
class AddLockingColumnsToOperators < ActiveRecord::Migration[5.1]
def up
add_column :operators, :lock_version, :integer, :default => 0, :null => false
end
def down
remove_column :operators, :lock_version
end
end
Can anyone tell me how to test the update action above with rspec?
Update: Here is the attempt I tried, but it didn't work
let!(:operator1) { FactoryBot.create(:operator, :site => site) }
let!(:attributes) {
{
id: operator1.id,
first_name: "test1",
last_name: "test2",
employee_number: "tesnt12345",
badge: "test215235",
suspended: true,
site_id: site.id
}
}
let!(:stale_attributes) {
{
id: operator1.id,
first_name: "test_fake",
last_name: "test_fake",
employee_number: "tesnt12345",
badge: "test215235",
suspended: true,
site_id: site.id
}
}
it("cause StaleObjectError when updating same operator at the same time") do
patch :update, params: { :id => operator1.id, :operator => attributes }
patch :update, params: { :id => operator1.id, :operator => stale_attributes }
expect(response).to raise_error(ActiveRecord::StaleObjectError)
end
You want a test case where optimistic locking fails.
First get your edit form.
Then update the record from an independent update.
Then submit the edit form.
In a functional test it might look like this:
visit "/operators/#{operator.id}/edit"
indep_operator = Operator.find(operator.id)
indep_operator.update!( ... some attributes ...)
fill_in "Name", :with => "New Value"
click_button "Update Operator"
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.
I have User (which is I used Devise) and Profile model where User has_one Profile as their relationship. I got an error when run the rspec test. Below is my spec to handle when user is updating his/her profile.
spec/controller/profiles_controller_spec.rb
RSpec.describe ProfilesController, type: :controller do
let(:profile) { FactoryGirl.create(:profile) }
let (:valid_attributes) { FactoryGirl.attributes_for(:profile) }
let (:invalid_attributes) { FactoryGirl.attributes_for(:profile).merge({fname: nil}) }
let(:user) { FactoryGirl.create(:user) }
let(:valid_session) { sign_in(user) }
describe "PUT #update" do
before { valid_session }
context "with valid attributes" do
it "saves valid profile" do
expect do
put :update, { id: profile.id, profile: { fname: "Cena" } }
end.to change{ profile.reload.fname }.to("Cena")
end
end
spec/factories/profiles.rb
FactoryGirl.define do
factory :profile do
user
fname "John"
lname "Doe"
avatar "my_avatar"
end
end
app/controller/profiles_controller.rb
class ProfilesController < ApplicationController
private
def user_params
params.require(:user).permit(:id, :login, :email,
profile_attributes: [
:id, :user_id, :fname, :lname, :avatar, :avatar_cache
])
end
end
And here is the error when run rspec spec/controllers/accounts_controller_spec.rb
Failures:
1) AccountsController PUT #update with valid attributes saves valid profile
Failure/Error: put :update, {id: profile.id, user_id: user.id, profile: { fname: "Cena" }}
ActionController::ParameterMissing:
param is missing or the value is empty: user
let(:user) { FactoryGirl.create(:user) }
let(:profile) { FactoryGirl.create(:profile, user_id: user.id ) }
describe "PUT #update" do
before { valid_session }
context "with valid attributes" do
it "saves valid profile" do
expect do
put :update, id: user.id, user: { profile_attributes: { user_id: user.id, fname: "Cena" } }
end.to change{ profile.reload.fname }.to("Cena")
end
end
end
The profiles_controller.rb code you posted is missing the update action (and also the class name is AccountController), but I guess you are doing something like user.update(user_params).
If that's the case, as the error says, the params passed from the controller spec does not have :user key, and that is causing the error.
Assuming from the #user_params method and the error, the params passed to the post in controller spec needs to look like the following:
{
user: {
id: xxx, ...,
profile_attributes: {
id: xxx,
fname: xxx, ...
}
}
}
Ive been forcing myself to learn Rails4 with BDD and so far its come along quite nicely. However, I have been hammering at this issue for a few hours now and not come up with a reason for why it isnt working. I am currently working on testing Controllers, with only controllers and models built, no forms or anything else. It works via console, so I must simply be using something wrong.
Here is my code for reference:
/admin/pages_controller.rb
class Admin::PagesController < ApplicationController
def update
#page = Page.find_by_id(params[:id])
#page.update_attributes!(message_params)
redirect_to edit_admin_page_path(#page)
end
private
def message_params
params.require(:page).permit(
:url,
:position,
:name,
:tags,
images_attributes:
[:image_file_name, :image_file_size, :image_content_type, :name, :caption, :tags, :owner_id, :owner_type],
block_attributes:
[:id, :body, :owner_id, :owner_type]
)
end
end
models/page.rb
class Page < ActiveRecord::Base
validates :name, presence: true
before_save :validate_url
has_many :images, :as => :owner
has_one :block, :as => :owner
accepts_nested_attributes_for :images, :block
def validate_url
if url.blank?
self.url = self.name.strip.downcase.gsub(" ","-").gsub(%r([^0-9a-z-]), '').gsub("--","-")
end
end
end
pages_controller_spec.rb
describe "POST #update" do
before :each do
#page = FactoryGirl.create(:page)
end
it "makes sure user can upload an image" do
image = FactoryGirl.build(:image, owner_id: #page.id, owner_type: "Page")
post :update, id: #page, page: { :images => [ image ]}
#page.reload
expect(#page.images.first).to eq(image)
end
it "updates values of the attributes accordingly" do
post :update, id: #page, page: { :name => 'foo', :url => 'bar' }
#page.reload
expect(#page.name).to eq('foo')
end
it "updates the values of the block" do
#page = FactoryGirl.create(:page_with_block)
post :update, id: #page, page: { block: { :body => 'foobar' } }
#page.reload
expect(#page.block.body).to eq('foobar')
end
end
spec/factories/blocks.rb
FactoryGirl.define do
factory :block do
body "<html><body><h1>Hello world!</h1><section>This is the content section</section></body></html>"
end
end
spec/factories/images.rb
include ActionDispatch::TestProcess
FactoryGirl.define do
factory :image do
image { fixture_file_upload Rails.root.to_s + '/spec/images/1.jpg', 'image/jpg'}
name { Faker::App.name }
caption { Faker::Lorem.sentence }
tags { Faker::Lorem.words }
end
end
RSpec results:
1) Admin::PagesController POST #update makes sure user can upload an image
Failure/Error: expect(#page.images.first).to eq(image)
expected: #<Image id: nil, image_file_name: "1.jpg", image_file_size: 15078, image_content_type: "image/jpg", name: "Alpha", caption: "Assumenda et exercitationem quo.", tags: ["doloribus", "maiores", "dicta"], owner_id: 37, owner_type: "Page", created_at: nil, updated_at: nil>
got: nil
(compared using ==)
# ./spec/controllers/admin/pages_controller_spec.rb:111:in `block (3 levels) in <top (required)>'
2) Admin::PagesController POST #update updates the values of the block
Failure/Error: expect(#page.block.body).to eq('foobar')
expected: "foobar"
got: "<html><body><h1>Hello world!</h1><section>This is the content section</section></body></html>"
(compared using ==)
# ./spec/controllers/admin/pages_controller_spec.rb:124:in `block (3 levels) in <top (required)>'
Finished in 3.95 seconds (files took 3.71 seconds to load)
19 examples, 2 failures
models/image.rb
class Image < ActiveRecord::Base
validates_presence_of :image_file_name, :image_file_size, :image_content_type
before_save :clean_up
belongs_to :page, foreign_key: "owner_id"
has_attached_file :image
validates_attachment_content_type(:image, :content_type => /^image\/(jpg|jpeg|pjpeg|png|x-png|gif)$/)
validates :image, :attachment_presence => true
def clean_up
if name.blank?
self.name = self.image_file_name.strip.downcase.gsub(" ","-").gsub(%r([^0-9a-z-]), '').gsub("--","-")
end
end
end
Any help would be appreciated on this one, as most of the searches ive found about this subject deal with forms or params.require. I am able to update the attributes of the page itself with the second spec, but nothing I do seems to make it into the nested resources. What am I missing here? Thanks in advance!
Edit: updated to include image model
First of all, avoid using fixture file upload with factory_girl http://pivotallabs.com/avoid-using-fixture-file-upload-with-factorygirl-and-paperclip/
You can simple use it like the following:
FactoryGirl.define do
factory :image do
image { File.new(Rails.root.join('spec/images/1.jpg') }
name { Faker::App.name }
caption { Faker::Lorem.sentence }
tags { Faker::Lorem.words }
end
end
It's not a clear answer, it's just a way in the right direction:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new("spec/factories/files/file.csv") }
let!(:page) { FactoryGirl.create(:page) }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page)
end
let(:image) { page.images.first }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images_attributes => { "0" => image_params } }
end.to change { page.reload.images.count }.by(1)
# Now compare the attributes
expect(image.content_type).to eq(image_attrs[:some_type])
# ...
end
it "updates values of the attributes accordingly" do
expect do
post :update, id: page, page: { name: 'foo', url: 'bar' }
end.to change { page.reload.name }.to("foo")
end
context "updates the values of the block" do
let(:page) { FactoryGirl.create(:page_with_block) }
it do
expect do
post :update, id: page, page: { block: { :body => 'foobar' } }
end.to change { page.reload.block.body }.to("foobar")
end
end
end
Changed the spec to this due to comment:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new('spec/images/1.jpg') }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page, image: img)
end
let!(:page) { FactoryGirl.create(:page_with_block) }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images => { "0" => image_attrs } }
end.to change { page.reload.images.count }.by(1)
end
it "updates values of the attributes accordingly" do
post :update, id: page, page: { :name => 'foo', :url => 'bar' }
page.reload
expect(page.name).to eq('foo')
end
it "updates the values of the block" do
expect do
post :update, id: page, page: { block: { :body => 'foobar' } }
end.to change { page.reload.block.body }.to("foobar")
end
end
Problem still occurs:
................F.F
Failures:
1) Admin::PagesController POST #update makes sure user can upload an image
Failure/Error: expect do
expected result to have changed by 1, but was changed by 0
# ./spec/controllers/admin/pages_controller_spec.rb:111:in `block (3 levels) in <top (required)>'
2) Admin::PagesController POST #update updates the values of the block
Failure/Error: expect do
expected result to have changed to "foobar", but did not change
# ./spec/controllers/admin/pages_controller_spec.rb:123:in `block (3 levels) in <top (required)>'
Finished in 1.32 seconds (files took 1.34 seconds to load)
19 examples, 2 failures
Failed examples:
rspec ./spec/controllers/admin/pages_controller_spec.rb:110 # Admin::PagesController POST #update makes sure user can upload an image
rspec ./spec/controllers/admin/pages_controller_spec.rb:122 # Admin::PagesController POST #update updates the values of the block
EDIT:
It turns out I didnt properly allow for image_attributes to work well with paperclip and basicly a lot of minor things were wrong. I was more used to how the setup was with Rails 3, and misinterpreted how things for Rails 4 gems work.
Updated spec:
describe "POST #update" do
let(:img) { Rack::Test::UploadedFile.new(Rails.root.join('spec/images/1.jpg'), 'image/jpg') }
let(:image_attrs) do
FactoryGirl.attributes_for(:image, owner: page, image: img)
end
let(:block_attrs) do
FactoryGirl.attributes_for(:block, owner: page, :body => 'foobar')
end
let!(:page) { FactoryGirl.create(:page_with_block) }
it "makes sure user can upload an image" do
expect do
post :update, id: page, page: { :images_attributes => { "0" => image_attrs } }
end.to change { page.reload.images.count }.by(1)
end
it "updates values of the attributes accordingly" do
post :update, id: page, page: { :name => 'foo', :url => 'bar' }
page.reload
expect(page.name).to eq('foo')
end
it "updates the values of the block" do
expect do
post :update, id: page, page: { :block_attributes => block_attrs }
end.to change { page.reload.block.body }.to("foobar")
end
end
Thanks to #ole for pointing me in the right direction for testing code!