I am new to Rails and I want to test my set strong parameters of the Book model with a controller test. I am using Minitest and Rails 4.
Book model:
class Book < ActiveRecord::Base
validates :title, presence: true, length: { in: 1..150 }
end
Book controller wit params:
def create
#book = Book.new book_params
if #book.save
redirect_to action: "index", notice: 'Success.'
else
render :new
end
end
private
def book_params
params.require(:book).permit(:title, :cover_image)
end
My idea for a test - does fail, because it creates an entry:
assert_no_difference('Book.count') do
post :create, book: {
id: 123,
title: "Lorem ipsum"
}
end
How can I get the tests go green and is it correct to test the strong parameters with a controller test?
I am looking for an answer to almost the same question. When using Rails 5 I eventually came up with a solution (call it workaround if you like :-) for testing that the unwanted params don't actually get through. In my (simplified here) case I want to disallow some "security critical" params being passed through when creating a new user.
In the controller (permitting only email and password):
private
def user_params
params.require(:user).permit(:email, :password)
end
In the integration test:
test "not permitted signup data submitted" do
new_user_email = "tester_" + (0...10).map { ('0'..'9').to_a[rand(26)] }.join + "#testing.net"
get signup_path
assert_difference 'User.count', 1 do
post signup_path, params: { user: { email: new_user_email, password: "testpassword", role_id: 1 } }
end
user = User.last
assert user.email == new_user_email
assert user.role_id == nil
end
Here I submit an additional, "sensitive" parameter role_id with the value of 1 (admin). I expect the user to be created. Then I read that newly (last) created user and expect it to have role_id empty (nil). To make the test fail I add :role_id to user_params. Removing it, makes the test pass. Obviously if your attribute can't be nil (aka NULL in SQL), you can test for default value being stored instead of the submitted one.
Since Rails drops all unpermitted parameters not in permit, the new record will be created, hence the test will be red.
Although, one can raise an exception with the action_on_unpermitted_parameters method when non-whitlisted parameters are submitted.
I do like to test Strong Parameters in the controller. I also like to test them more directly, so this is how I do it.
First, I have a test helper that is required in my test/test_helper.rb file:
test/test_helpers/controller_strong_params_helper.rb
# frozen_string_literal: true
module ControllerStrongParamsHelper
def assert_requires_param(param, &block)
#controller.params = ActionController::Parameters.new()
assert_raises(ActionController::ParameterMissing) { yield }
#controller.params = ActionController::Parameters.new(stub_parameter: {})
assert_raises(ActionController::ParameterMissing) { yield }
# It's not enough to have an empty required parameter, there needs to be something inside.
#controller.params = ActionController::Parameters.new(param => {})
assert_raises(ActionController::ParameterMissing) { yield }
#controller.params = ActionController::Parameters.new(param => '')
assert_raises(ActionController::ParameterMissing) { yield }
#controller.params = ActionController::Parameters.new(param => {something_inside: 'something'})
assert_nothing_raised { yield }
end
end
This lets me easily test the strong params that are not optional.
Now assume I have these strong params in my ExampleController:
def example_params
params.require(:example).permit(:id,
:name,
:description)
end
private :example_params
This is what my minitest tests would look like:
test/controllers/example_controller_test.rb
###############################################
test '#example_params should require an example parameter' do
assert_requires_param(:example) { #controller.send(:example_params) }
end
###############################################
test '#example_params should permit some expected example parameters' do
# Using hash rockets so the equality check works.
expected_permitted_params = { 'id' => nil,
'name' => nil,
'description' => nil }
# Specifically merge in any potential non-attribute parameters here if present/needed.
all_params = { example: Example.new.attributes }
#controller.params = ActionController::Parameters.new(all_params)
actual_permitted_params = #controller.send(:example_params)
assert_equal(expected_permitted_params, actual_permitted_params)
end
Related
In a controller I have an update method which creates a record (call it book), associates it to an existing record (call it author) and saves it.
Book belongs to one Author
add_author_to_book_controller.rb
def update
#author = App::Models::Author.new(params)
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
add_author_to_book_controller_spec.rb
describe App::AddAuthorToBookController do
describe '#update' do
# this is a contrived example, there is more setup regarding creating the "book" properly...
let(:name) { 'foobar' }
let(:action) { xhr :put, :update, params }
let(:params) { { first_name: name } }
subject { book }
before { action }
it { expect(response.status).to eq 200 }
it 'should save the author to the book' do
# why is author nil here?
# puts book.author.inspect
expect(book.author.first_name).to eq name
end
end
end
I tried book.reload in the test but that didn't work. I'm new to rails, is there some conventional way of testing an associated record in a controller test?
Wasn't saving author before associating it to book...
def update
#author = App::Models::Author.new(params)
# was simply missing this
#author.save!
#book = App::Models::Book.where(id: params[:book_id]).first
#book.author = #author
#book.save!
# this works fine...
# puts #book.author.inspect
render json: { status: :ok }
end
First of all I recommend you to make your controllers more general because that is the correct architecture you need to follow, so your controller can be called authors_controller.rb and manage all authors stuff or books_controller.rb and manage all books stuff. And following this approach you can have a method associate_book which receives an author and a book and creates the right association. Let me explain it with code:
class Author < ApplicationRecord
has_many :books
# Fields :name
validates :name, presence: true
end
class Book < AoplicationRecord
# Optional because I think you want to add the author after create it
belongs_to :author, optional: true
# Fields :title, :publish_year, :author_id
validates :title, :publish_year, :author_id, presence: true
end
class AuthorsController < ApplicationController
def associate_book
# params here will contain the following [:author_id, book_id]
author = Author.find(params[:author_id])
book = Book.find(params[:book_id])
book.author = author
book.save!
rescue ActiveRecord::RecordInvalid => error
# This will allow you to catch exceptions related to the update
end
end
Then you can test this method by doing the following, supposing that this method will be called from a route
# Testing with RSpec
# spec/controllers/authors_controller.rb
RSpec.describe AuthorsController do
let(:author) { Author.first }
let(:book) { Book.first }
it 'Should associate an author with a provided book' do
expect do
post :associate_book, params: { author_id: author.id, book_id: book.id }
end.to change { author.books.count }.by(1)
end
end
This will check the total count of books associated to the author.
I am trying to test a service but something wrong is happening when I pass the parameters to the service class, the values are being passed in the wrong way by rspec.
My service is:
class CheckInvitesService
def initialize(user, course)
#user = user
#course = course
end
def call
if UserCourseRegistration.exists?(user_id: #user, course_id: #course)
false
else
UserCourseRegistration.create(user_id: #user,
course_id: #course,
school_id: find_school)
end
end
private
def find_school
school = Course.find(#course).school.id
end
end
My test is:
require 'rails_helper'
RSpec.describe CheckInvitesService do
describe "call" do
context 'invite already exists' do
it 'return' do
#current_user_admin = create(:admin)
#school = create(:school, user: #current_user_admin)
#course = create(:course, user: #current_user_admin, school: #school)
# puts #course
# puts #course.id
#verify = CheckInvitesService.new(#course.id, #current_user_admin.id).call
expect(#verify).to be_falsey
end
end
end
end
I printed #course.id and it returns: 122
But when I call the service class, the parameter inside it has another value, for example the #course.id, i passed takes the value: 627
I get the following error:
Failures:
1) CheckInvitesService call invite already exists return
Failure/Error: school = Course.find(#course).school.id
ActiveRecord::RecordNotFound:
Couldn't find Course with 'id'=627
What is entering the class is another id of course the id 627 and not the 122 that should have been passed via parameter.
It appears your arguments are out of order. CheckInvitesService has:
initialize(user, course)
But when you create the CheckInvitesService object, you're passing course as the first argument.
CheckInvitesService.new(#course.id, #current_user_admin.id).call
Should be
CheckInvitesService.new(#current_user_admin.id, #course.id).call
You can use find_or_create_by in your service, it should work, too.
def call
UserCourseRegistration.find_or_create_by(user: #user, course: #course, school: find_school)
end
I am trying to write Rspec test case for the submit method in the app/controllers.sample.rb file.
User class is defined in the lib/classes folder. User object is created in the session.rb file in app/controllers/concerns which is autoloaded during creation of new session.
The user method in the session.rb gets the user parameters from another API.
Here I am finding it difficult to create the User object using Rspec, it is always returning error at
list = user.get_list
I have given the sample set of code I have written for the test case.
Could anyone help how to instantiate the User object in concerns/session.rb from rspec ?
app/controllers/concerns/session.rb
def user
if user
user
else
begin
rest_resource = RestClient::Resource.new(ENV['SESSION_API'], :verify_ssl => false)
data = rest_resource.get Authorization: request.headers['Authorization']
rescue RestClient::Exception => e
#error = JSON.parse(e.response, symbolize_names: true)
return nil
end
self.user = User.new(current_user,request.headers['Authorization'] )
end
end
spec/controllers/rspec_sample.rb
describe "Submit" do
it "Submit and expects to succeed" do
allow_any_instance_of(Concerns::Session).to receive(:current_user).and_return(name: "test")
allow_any_instance_of(Concerns::Session).to receive(:user).and_return(name: "test")
post :submit, params
expect(response).to have_http_status(200)
end
end
app/controllers/sample.rb
def submit
list = user.get_list
end
lib/classes/user.rb
class User
def list
return values
end
end
What is the actual strategy to test strong params filtering in Rails controller with Rspec? (Except shoulda matchers) How to write failing test and then make it green?
Create 2 hashes with expected and all (with unsatisfied) parameters. Then pass all params to action and check that you object model receiving only expected params. It will not if you are not using strong parameter filters. Than add permissions to params and check test again.
For example, this:
# action
def create
User.create(params)
end
# spec
it 'creates a user' do
expect_any_instance_of(User).to receive(:create).
with({name: 'Sideshow Bob'}.with_indifferent_access)
post :create, user:
{ first_name: 'Sideshow', last_name: 'Bob', name: 'Sideshow Bob' }
end
will pass all params to User and test will fail. And when you filter them:
def user_params
params.require(:user).permit(:name)
end
and change action with User.create(user_params), test will pass.
I personally use shoulda-matcher from thoughtbot.
With something like:
it do
should permit(:first_name, :last_name, :email, :password).
for(:update, params: params)
end
Here is how I did it:
describe 'Safe Params' do
let(:mixed_params) {
{
blueprint_application_environment: {
id: 1000,
blueprint_id: 1,
application_id: 2,
environment_id: 3
},
format: :json
}
}
context "when processing a Post request with a mix of permitted and unpermitted parameters" do
before { post :create, mixed_params }
it "a create will not set the value of the unpermitted parameter" do
expect(JSON.parse(response.body)["id"]).not_to eq(1000)
end
it "a create will set the value of the permitted parameters" do
expect(JSON.parse(response.body)["blueprint_id"]).to eq(1)
expect(JSON.parse(response.body)["application_id"]).to eq(2)
expect(JSON.parse(response.body)["environment_id"]).to eq(3)
end
end
end
Controller code:
def create
#blueprint_application_environment = BlueprintApplicationEnvironment.new(blueprint_application_environment_params)
if #blueprint_application_environment.save
render 'show.json.jbuilder'
else
render json: #blueprint_application_environment.errors, status: :unprocessable_entity
end
end
def blueprint_application_environment_params
params.require(:blueprint_application_environment).permit(:blueprint_id, :application_id, :environment_id)
end
as like you create or update object using strong parameters,it is also similar except one thing that normal you do like this:
post :create, book_id: #book.id
But in strong parameter you have to do like this:
post :create, {book_id: #book.id, comment: {user_id: 101, book_id:
#book.id, description: "worth to buy"}}
you have to pass in nested parameters.
let(:product) { FactoryGirl.create(:product) }
it "should blah" do
product.name = "a"
product.save!
post :update, id: product.id, product: { name: "x" }
# assuming :update changes the product's name to params[:name]
product.reload.name.should == "x"
end
The should always fails unless I do something like
Product.find(product.id).name.should == "x"
Am I misusing let?
If I work with #product created within before :each and #product.reload it's fine.
If you break during execution of a spec and call self.class.method(:let).source, you get something like:
def let(name, &block)
define_method(name) do
__memoized.fetch(name) {|k| __memoized[k] = instance_eval(&block) }
end
end
Upon further inspection, __memoized is just a simple Hash. I don't think there's anything fancy going on here. What precise versions of rails and rspec were you using?