Search code examples
ruby-on-railsrspecstrong-parameters

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

Solution

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