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.
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.