How to test strong params with Rspec? - ruby-on-rails

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.

Related

Rails strong parameters - Request allowed without required key

I'm working on a Rails API and I'm using strong parameters in the controllers. I have a request spec that is failing for one model but works on all other models. The controllers for each model are pretty much all the same.
As you can see below in the spec, the request body SHOULD be { "tag": { "name": "a good name" }}. However, this spec is using { "name": "a good name" } which SHOULD be invalid because it's missing he "tag" key. This same spec for the same controller functionality works fine for plenty of other models.
Another interesting twist is that if I change the controller's strong parameter to params.require(:not_tag).permit(:name) it throws an error for not including the "not_tag" key.
Ruby: 2.6.5p114
Rails: 6.0.1
Expected response status: 422
Received response status: 201
Controller
class TagsController < ApplicationController
before_action :set_tag, only: [:show, :update, :destroy]
# Other methods...
# POST /tags
def create
#tag = Tag.new(tag_params)
if #tag.save
render "tags/show", status: :created
else
render json: #tag.errors, status: :unprocessable_entity
end
end
# Other methods...
private
# Use callbacks to share common setup or constraints between actions.
def set_tag
#tag = Tag.find_by(id: params[:id])
if !#tag
object_not_found
end
end
# Only allow a trusted parameter "white list" through.
def tag_params
params.require(:tag).permit(:name)
end
# render response for objects that aren't found
def object_not_found
render :json => {:error => "404 not found"}.to_json, status: :not_found
end
end
Request Spec
require 'rails_helper'
include AuthHelper
include Requests::JsonHelpers
RSpec.describe "Tags", type: :request do
before(:context) do
#user = create(:admin)
#headers = AuthHelper.authenticated_header(#user)
end
# A bunch of other specs...
describe "POST /api/tags" do
context "while authenticated" do
it "fails to create a tag from malformed body with 422 status" do
malformed_body = { "name": "malformed" }.to_json
post "/api/tags", params: malformed_body, headers: #headers
expect(response).to have_http_status(422)
expect(Tag.all.length).to eq 0
end
end
end
# A bunch of other specs...
after(:context) do
#user.destroy
#headers = nil
end
end
This behaviour is because of the ParamsWrapper functionality which is enabled by default in Rails 6. wrap_parameters wraps the parameters that are received, into a nested hash. Hence, this allows clients to send requests without nesting data in the root elements.
For example, in a model named Tag, it basically converts
{
name: "Some name",
age: "Some age"
}
to
{
tag:
{
name: "Some name",
age: "Some age"
}
}
However, as you see in your test, if you change the required key to not_tag, the wrapping breaks the API call, as expected.
This configuration can be changed using the config/initializers/wrap_parameters.rb file. In that file, you could set wrap_parameters format: [:json] to wrap_parameters format: [] to disallow such wrapping of parameters.

How to test that a class is called in a controller method with RSpec

I am testing my controller to ensure that a library class is called and that the functionality works as expected. NB: This might have been asked somewhere else but I need help with my specific problem. I would also love pointers on how best to test for this.
To better explain my problem I will provide context through code.
I have a class in my /Lib folder that does an emission of events(don't mind if you don't understand what that means). The class looks something like this:
class ChangeEmitter < Emitter
def initialize(user, role, ...)
#role = role
#user = user
...
end
def emit(type)
case type
when CREATE
payload = "some payload"
when UPDATE
payload = "some payload"
...
end
send_event(payload, current_user, ...)
end
end
Here is how I am using it in my controller:
class UsersController < ApplicationController
def create
#user = User.new(user_params[:user])
if #user.save
render :json => {:success => true, ...}
else
render :json => {:success => false, ...}
end
ChangeEmitter.new(#user, #user.role, ...).emit(ENUMS::CREATE)
end
end
Sorry if some code doesn't make sense, I am trying to explain the problem without exposing too much code.
Here is what I have tried for my tests:
describe UsersController do
before { set_up_authentication }
describe 'POST #create' do
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
post :create, user: user_params
expect(response.status).to eq(200)
// Here is the test for the emitter
expect(ChangeEmitter).to receive(:new)
end
end
end
I expect the ChangeEmitter class to receive new since it is called immediately the create action is executed.
Instead, here is the error I get:
(ChangeEmitter (class)).new(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
What am I missing in the above code and why is the class not receiving new. Is there a better way to test the above functionality? Note that this is Rspec. Your help will be much appreciated. Thanks.
You need to put your expect(ChangeEmitter).to receive(:new) code above the post request. When you are expecting a class to receive a method your "expect" statement goes before the call to the controller. It is expecting something to happen in the future. So your test should look something like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
expect(ChangeEmitter).to receive(:new)
post :create, user: user_params
expect(response.status).to eq(200)
end
EDIT
After noticing that you chain the "emit" action after your call to "new" I realized I needed to update my answer for your specific use case. You need to return an object (I usually return a spy or a double) that emit can be called on. For more information on the difference between spies and doubles check out:
https://www.ombulabs.com/blog/rspec/ruby/spy-vs-double-vs-instance-double.html
Basically a spy will accept any method called on it and return itself whereas with a double you have to specify what methods it can accept and what is returned. For your case I think a spy works.
So you want to do this like:
it "calls the emitter" do
user_params = FactoryGirl.attributes_for(:user)
emitter = spy(ChangeEmitter)
expect(ChangeEmitter).to receive(:new).and_return(emitter)
post :create, user: user_params
expect(response.status).to eq(200)
end

Rails 5 scaffold and Rspec testing for an enum in Controller test

I just built a Rails 5 new app --api. I scaffolded a model and added an enum.
class Track < ApplicationRecord
enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
end
One of the scaffolded controller test looks like this:
context "with invalid params" do
it "assigns a newly created but unsaved track as #track" do
post :create, params: {track: invalid_attributes}, session: valid_session
expect(assigns(:track)).to be_a_new(Track)
end
end
I added invalid attributes at the top:
let(:invalid_attributes) {{
"name": "Brands Hatch",
"surface_type": "wood"
}}
and changed the expect line to this
expect(assigns(:track)).not_to be_valid
But the test does not work, because its not possible to create a Track object if you pass an invalid enum.
Controller action:
def create
#track = Track.new(track_params)
if #track.save
render json: #track, status: :created
else
render json: #track.errors, status: :unprocessable_entity
end
end
So how do I test this scenario?
One way you could trap an invalid :surface_type through normal validation is by intercepting the assignment.
class Track < ApplicationRecord
enum surface_type: [:asphalt, :gravel, :snow], _prefix: true
attr_accessor :bad_surface_type
validate :check_surface_type
def surface_type=(surface)
super surface
rescue
self.bad_surface_type = surface
super nil
end
private
def check_surface_type
errors.add(:surface_type, "the value #{bad_surface_type} is not valid") if bad_surface_type
end
end

Test strong parameters with minitest in Rails 4

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

Testing letsrate controller. RSpec

I want to test letsrate generated controller.
But I don;t know how do this, because I can not understand how it works.
rater_controller.rb
class RaterController < ApplicationController
def create
if user_signed_in?
obj = params[:klass].classify.constantize.find(params[:id])
obj.rate params[:score].to_i, current_user, params[:dimension]
render :json => true
else
render :json => false
end
end
end
UPDATE
Letsrate is a gem for rails
rater_controller_spec.rb
require 'rails_helper'
describe RaterController do
describe 'POST create' do
let(:valid_attributes)do {
klass: 'Hotel',
dimension: 'rating',
score: '5'
}
end
it 'user signed in' do
user = create(:user)
hotel = create(:hotel)
post :create, { rate: valid_attributes, rater_id: user.id, rateble_id: hotel.id }
sign_in user
end
end
end
The source code you posted makes it pretty obvious how it works. You need to call the create action in RaterController with these params: klass, id, score, dimension. Let's say the klass param is "Restaurant", which is also the name of an ActiveRecord model class. The controller will query the database for a restaurant with the specified ID. Then it will call the rate method on that object with the specified parameters, which presumably inserts a row into the database representing the user's new rating. To test it, you could simply call the controller action and then check to make sure the row got added to the database.

Resources