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'm writing the code to get my Rspec tests to pass on my api. I'm using the apipie gem to generate documentation and it seems that my tests are failing because thy are expecting a number and it's funny because this is exactly what I want to test.
The page fails when the :bpm parameter is not a number. is there any way of going around this ?
context "when is not created" do
before(:each) do
user = FactoryGirl.create :user
#invalid_lesson_attributes = { title: "California Dreamin",
bpm: "Hello"
}
request.headers['Authorization'] = user.auth_token
post :create, { user_id: user.id, lesson: #invalid_lesson_attributes }
end
it "renders an errors json" do
lesson_response = json_response
expect(lesson_response).to have_key(:errors)
end
it "renders the json errors on why the user could not be created" do
lesson_response = json_response
expect(lesson_response[:errors][:bpm]).to include "is not a number"
end
it { should respond_with 422 }
end
end
Update spec:
context "when is not updated" do
before(:each) do
patch :update, { user_id: #user.id, id: #lesson.id,
lesson: { bpm: "ten" }, format: :json }
end
it "renders an errors json" do
lesson_response = json_response
expect(lesson_response).to have_key(:errors)
end
it "renders the json errors on why the user could not be updated" do
lesson_response = json_response
expect(lesson_response[:errors][:bpm]).to include "is not a number"
end
it { should respond_with 422 }
end
in my users_controller:
api :POST, '/teachers/:user_id/lessons/', "Create lesson"
param :lesson, Hash, desc: 'Lesson information', :required => true do
param :title, String, desc: 'Title of the lesson', :required => true
param :bpm, :number, desc: 'tempo of the lesson (beats per second)', :required => true
end
error :code => 422, :desc => "Unprocessable Entity"
my error when I run my rspec tests :
Apipie::ParamInvalid: Invalid parameter 'bpm' value "Hello": Must be a number.
Adds format json to post request
post :create, { user_id: user.id, lesson: #invalid_lesson_attributes, format: :json }
That worked for me.
I'm writing a Rails API and am stuck trying to write controllers that will test the authentication. For instance, I have in my PostController before_action :authenticate which is
def authenticate
authenticate_or_request_with_http_token do |token, options|
User.find_by(:auth_token => token)
end
end
And this is my PostsController test:
require 'rails_helper'
RSpec.describe Api::PostsController, type: :controller do
let(:valid_attributes) {
{
"title" => "Post title",
"content" => "Post content",
"status" => "published"
}
}
let(:invalid_attributes) {
{
"title" => "",
"content" => "",
"status" => ""
}
}
let(:valid_session) { {} }
before do
params = { session: { email: 'wagner.matos#mac.com', password: 'foobar' } }
SessionsController.create params
#post = Post.new({
title: "Some title",
content: 'Some content',
status: "published"
})
end
it "creates a new post" do
post :create, post: #post
expect(Post.count).to eq(1)
end
end
Yet the above is failing with the following error:
1) Api::PostsController creates a new post
Failure/Error: SessionsController.create params
NoMethodError:
undefined method `create' for SessionsController:Class
Any suggestions?
You can not invoke create action of SessionsController with class. Better you create user object and assign to request.env the same token. like below sample code -
require 'rails_helper'
RSpec.describe Api::PostsController, type: :controller do
before do
user = User.create( :auth_token => 'token' )
request.env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Token.encode_credentials("token")
end
end
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!
I thought it might be like:
let(:organization) { mock_model(Organization).as_null_object }
before(:each) do
Organization.stub(:find).and_return(organization)
end
it "calls the destroy action on #organization" do
assigns[:organization].should_receive("destroy")
post :destroy, :id => organization.id
end
..but I get a "can't modify frozen object" error.
Here's how I would write that spec:
describe 'Oragnization#destroy' do
let(:organization) { mock_model(Organization, :id => 1, :destroy => true) }
subject { post :destroy, :id => organization.id }
it { should be_successful }
end