Testing JSON API Strong Parameters in Rails

Recently I was experiencing a problem with a Rails API where I would update a model with new attributes, and then proceeded to add the new attributes to the model's factory, and then add the corresponding model and request specs.

The problem was that I would forget to whitelist the new attributes in the controller. Then running the request specs would not detect the error since I only test against one updated attribute that was whitelisted long ago. This became really annoying since I had no way to know if the new attributes were being whitelisted or not, and my tests weren't saying anything about it. This problem becomes even more apparent when the list of whitelisted attributes starts becoming very long.

In an API following the JSON API specification, here is an example of how the strong parameters method would look like:

def thing_params
  params.require(:data).require(:attributes).permit(:color, :size, :age, :name,
      :owner, :origin, :location, :purpose, :price, :alive)
end

As you can see, the list of attributes is getting pretty long. And we do not want to explicitly keep adding more attributes to the test.

Testing Strong Parameters in Controller Specs

After the release of Rails 5 I started ditching controller specs in favor of request specs. I like this approach and I think it's great, but I think it would be too much unecessary work to test that all the necessary attributes are whitelisted, in the request spec. Therefore I proceeded to create very simple controller specs that would test that all attributes of importance are whitelisted in the controller.

To achieve this, I use the great shoulda-matchers library. Specifically, its permit matcher. This matcher will help us test whitelisting of the given attributes.

In the end, here's how the controller spec would look like. I will go over the important details and tricks after:

RSpec.describe ThingsController, type: :controller do

  let(:attributes) { FactoryGirl.attributes_for(:thing).keys }
  let(:params) { { data: { type: 'things', attributes: FactoryGirl.attributes_for(:thing) } } }

  describe 'POST create' do
    it { should permit(*attributes).for(:create, params: params).on(:data).on(:attributes) }
  end

end

The above is the approach I reached and I am pretty satisfied with. Please leave me some comments if you think this approach is not a good idea.

Anyways, this approach has some very neat things:

Implicit Attributes List

Thanks to this line:

let(:attributes) { FactoryGirl.attributes_for(:thing).keys }

We can obtain the list of attributes with the help of FactoryGirl. This means that whenever we add new attributes to the model, and then add these new attributes to the factory, this test will fail if we do not whitelist these attributes in thing_params. This is exactly what we want.

The only thing I don't like about this is the expectation failure message:

Failure/Error: it { should permit(*attributes).for(:create, params: params).on(:data).on(:attributes) }

 Expected POST #create to restrict parameters on :attributes to :color, :size, :age, :name,
      :owner, :origin, :location, :purpose, :price, :alive, and :important,
 but the restricted parameters were :color, :size, :age, :name, :owner, :origin, :location, :purpose, :price, and :alive instead.

Makes it a bit hard to know which attributes are the new non-whitelisted attributes.

Not an Array

Obtaining the keys from the hash generated by FactoryGirl will give us an array of the keys. Even though permit() accepts a list (so to speak) of the attributes, it does not receive an array.

This is where Ruby's splat operator comes in. It allows us to pass a non-fixed amount of attributes to the method, without sending them in an array:

permit(*attributes)

Simple Parameters Definition and Passing

In order to test permtited parameters using permit(), we must define the parameters object and then pass it. Since our attributes list is already pretty long, and it's possible it will get even longer, explicitly listing the parameters object seems like a tedious idea. Fortunately I found a way to easily define it:

let(:params) { { data: { type: 'things', attributes: FactoryGirl.attributes_for(:thing) } } }

Again, we are doing this thanks to the help of FactoryGirl. Also notice that this params object is constructed according to JSON API.

We can then easily pass this object to permit():

permit(*attributes).for(:create, params: params)

Permiting and Requiring

Since we are using JSON API specification, a few things have to be changed in order to conform to the require in the strong parameters method. In shoulda matcher's permit(), this can be done by adding multiple on() methods. Similar to the chaining of multiple require() methods in the controller:

.on(:data).on(:attributes)

Conclusions

I feel this is a pretty neat approach to testing strong parameters in a Rails JSON API and being confident that adding new attributes to your models will not make you forget whitelisting them, thanks to failing tests.

If you think this is a bad approach, or that it could be imporved, please let me know!

References

  1. Splat Operator in Ruby
rspec json rails tdd bdd web-dev backend

Comments

comments powered by Disqus