Rails strong parameters - Request allowed without required key - ruby-on-rails

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.

Related

How to use the allow method in RSpec to mock a function inside a controller that is inside a module (Module > Controller > Function)

I am trying to write the allow method in RSpec. My rails controller is
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
if response['code'] == 200
if User.first.update(user_params)
render json: { message: "User successfully updated"}, status: :ok
else
head :unprocessable_entity
end
else
render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
end
end
private
def send_request_to_update_in_comapny(payload)
response = Api::V1::CompanyRequestService.new(
payload: payload.merge(company_api_access_details),
url: 'customers/update_name_email',
request_method: Net::HTTP::Post
).call
JSON.parse(response.body)
end
end
end
When I write the bellow code in my test file
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company).and_return({ 'code' => 500 })
I am getting the following error in terminal
Users::ProfilesController does not implement: send_request_to_update_in_comapny
enter code here
With allow_any_instance_of I am able to get the code working. But how can I implement it using allow?
Yes, allow_any_instance_of works because, as the name suggests, it allows any instance of Users::ProfilesController to respond to the instance method send_request_to_update_in_company with your mock return value.
However, your line
allow(Users::ProfilesController).to receive(:send_request_to_update_in_company)
is telling RSpec to mock a class method called send_request_to_update_in_company, which doesn't exist. And so, you're seeing the error message saying so.
You don't say where your test is situated, but generally wherever it is, it's not a good idea to either test or stub out a private method.
I'd be inclined to instead create a mock Api::V1::CompanyRequestService object to return a fake response, which your controller code can then parse as expected and produce the expected JSON. For example
mock_request = instance_double(Api::V1::CompanyRequestService)
allow(mock_request).to receive(:call).and_return('{"code": 500}')
allow(Api::V1::CompanyRequestService).to receive(:new).and_return(mock_request)
Another approach might be to leave your service alone, and instead use tools like VCR or WebMock to provide mocked JSON values at the network layer - your code can think it's calling out to the internet, but really it gets back responses that you define in your tests.
How about this way:
spec/requests/users/profiles_controller_spec.rb
require 'rails_helper'
RSpec.describe "Users::ProfilesControllers", type: :request do
describe "Test call to special function: " do
let(:controller) { Users::ProfilesController.new }
it "Should response to code 500" do
response = controller.send_request_to_update_in_company("test")
expect(response).to eq({"code"=>"500", "test1"=>"abc", "test2"=>"def"})
end
it "Should return to true" do
response = controller.true_flag?
expect(response).to eq(true)
end
end
end
app/controllers/users/profiles_controller.rb
module Users
class ProfilesController < ApplicationController
# Update user profile
def update
payload = { name: params[:user][:name],email: params[:user][:email]}
response = send_request_to_update_in_company(payload)
Rails.logger.debug "Ok71 = response['code'] = #{response['code']}"
# if response['code'] == 200
# if User.first.update(user_params)
# render json: { message: "User successfully updated"}, status: :ok
# else
# head :unprocessable_entity
# end
# else
# render json: { error: 'Error updating user in Company' },status: :unprocessable_entity
# end
end
# Not private, and not mistake to 'send_request_to_update_in_comapny'
def send_request_to_update_in_company(payload)
response = Api::V1::CompanyRequestService.new(
payload: "for_simple_payload_merge_values",
url: 'for_simple_customers/update_name_email',
request_method: "for_simple_request_method"
).call
Rails.logger.debug "Ok66 = Start to log response"
Rails.logger.debug response
JSON.parse(response.body)
end
# Simple function to test
def true_flag?
true
end
end
end
app/services/api/v1/company_request_service.rb
class Api::V1::CompanyRequestService < ActionController::API
def initialize(payload="test1", url="test2", request_method="test3")
#payload = payload
#url = url
#request_method = request_method
end
def call
#object = Example.new
#object.body = {code: "500", test1: "abc", test2: "def"}.to_json
return #object
end
end
class Example
attr_accessor :body
def initialize(body={code: "000", test1: "init_value_abc", test2: "init_value_def"}.to_json)
#body = body
end
end
I use simple code to simulate your project. Modify it to suitable your working! Tell me about your its thinking. Thank you!

Make params required in a rails api

I have a controller that accepts three params, title, users and project_type. I want to make all the params required
I have seen people do things like
def project_params
params.require(:title,:project_type, :users)
.permit(:title, :project_type, :users)
end
And then do Project.new(project_params), but I need to work a little with the params first. How can I make this possible?
I make a post request in postman like this:
module Api
module V1
class ProjectsController < ApplicationController
def create
admins = params[:admins]
users = get_user_array()
project_type = ProjectCategory.find_by(name: params[:project_type])
project = Project.new(
title: params[:title],
project_category: project_type,
project_users: users)
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
...
end
end
end
{
"title": "Tennis",
"project_type": "Sports",
"users": [{"name": "john Dow", "email": "johnDoe#gmail.com"}],
}
I would say that you are using ActionController::Parameters#require wrong. Its not meant to validate that the all the required attributes are present - thats what model validations are for. Rather you should just use params.require to ensure that the general structure of the parameters is processable.
For example if you used the rails scaffold you would get the following whitelist:
params.require(:project)
.permit(:title, :project_type)
This is because there is no point in continuing execution if the project key is missing from the params hash since this would give you an empty hash or nil.
ActionController::Parameters#require will raise a ActionController::ParameterMissing error which will return a 400 - Bad Request response which is the wrong response code for what you are doing. You also should not use exceptions for normal application flow. A missing attribute is not an exceptional event.
Instead if you want to use a flat params hash you should whitelist it with:
def project_params
params.permit(:title, :project_type, users: [:name, :email])
end
I think that if you don't have to get anything from the frontend to run get_user_array(), you could only allow and require title and project_type.
def create
users = get_user_array()
project = Project.new(project_params)
project.users = users
if project.save
render json: {data:project}, status: :ok
else
render json: {data:project.errors}, status: :unprocessable_entity
end
end
private
def project_params
params.require(:project).permit(:title, :project_type).tap do |project_params|
project_params.require(:title, :project_type)
end
end
If you need to process something before creating the project, you can do this:
project_category = ProjectCategory.find_by(name: project.project_type)

Dynamic rendering with Rails 5 and Rspec

I am trying to test that my controller renders the correct status codes via request specs. This application uses a bit of meta-programming with dynamic class names to render views. How can I stub the render call below to return the correct status code desired for my spec?
Rspec Spec Snippet
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:template) { "providers/v1/#{provider_slug}/new" }
let(:layout) { "providers/v1/#{provider_slug}" }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(described_class).to receive(:render_new_form).and_return(true)
get route
end
it 'true' do
expect(response.status).to be(200)
end
end
Controller Snippet
class V1::ProvidersController < ApplicationViewController
before_action :init_provider, :init_form_types, :validate_provider
def new
#provider_form_model = provider_router.form_model.new
render_new_form
end
private
attr_reader :provider_slug, :provider_path, :provider_router, :provider_model
def render_new_form
render template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}"
end
Updated for Answer Below
context 'renders 200' do
let(:provider_slug) { create(:provider, :active).slug }
let(:provider_double) do
instance_double(
ProviderRouter,
valid?: true,
form_model: ProviderFormModel
)
end
before do
allow(ProviderRouter).
to receive(:new).with(version: 'V1', provider_slug: provider_slug).
and_return(provider_double)
allow(controller).to receive(:provider_slug).and_return(provider_slug)
allow(controller).to receive(:render).and_call_original
allow(controller).to receive(:render).
with(template: "providers/v1/#{provider_slug}/new", layout: "providers/v1/#{provider_slug}") do
controller.render plain: '200 [OK]'
end
get "/v1/providers/#{provider_slug}"
end
it 'true' do
expect(response.status).to be(200)
end
end
The formal answer would be that you shouldn't stub it as you would be stubbing behaviour of the object under test.
You should rather provide a provider_slug to be used for the test.
Technically, it would be possible to do this:
allow(controller) # controller is the instance of the ProvidersController used under the hood
.to_receive(:render_new_form)
.and_return("some bogus value")
But this would lead to rails trying to render the default template as no rendering has happened yet. It would thus be helpful to actually call the render method which can be achived by:
# we call the render method in our stub and thus have to be able to call the original
allow(controller)
.to receive(:render)
.and_call_original
allow(controller)
.to_receive(:render) # not render_new_form
.with(template: anything, layout: anything) do
controller.render plain: '200 [OK]'
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

RSpec: setting request headers

I'm trying to set important headers with ActionDispatch request helper method in my specs:
RSpec.describe API::V1::FoosController, type: :request do
describe 'GET #index' do
context 'common case' do
request.env.merge!({'HTTP_FOO': 'FOO'})
get api_foos_path, {}, {Accept: Mime::JSON}
end
end
end
But this header (and generally any header set with request) disappears when it comes to controller:
class API::V1::FoosController < ApplicationController
respond_to :json, :xml
def index
request.env.has_key? 'HTTP_FOO' # false
respond_with serialize_models(Foo.all)
end
# ...
end
Why does it happen and how do I set it properly?
Setting the header with request.header or #request.header does the same.
P.S.: I know I can set headers as a third parameter of Rack::Test::Methods helpers, but I don't want to violate DRY - I'd like Mime formats only to be defined there.
Please try like this:
request.env['HTTP_FOO_HEADER'] = 'foo header'
Use controller.request.headers:
controller.request.headers['HTTP_FOO'] = 'FOO'
I can verify that this approach works in Rails 4.2.5, as this is lifted directly from real-world code.
Our tests look like this:
describe SomeController, type: :controller do
let(:current_user) { create :user }
before :each do
controller.request.headers['Authorization'] = "APIKey #{current_usser.api_key}"
end
end
And our ApplicationController looks (more or less) like this:
before_action :authenticate_request
def authenticate_request
key = request.headers['Authorization']
user = User.find_by(api_key: key)
# raise AuthenticationError unless user, etc etc
end

Resources